@simplysm/orm-common 13.0.68 → 13.0.70
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/README.md +54 -1447
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +9 -9
- package/dist/create-db-context.js.map +1 -1
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +2 -2
- package/dist/ddl/initialize.js.map +1 -1
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/table-ddl.d.ts +24 -24
- package/dist/ddl/table-ddl.js +4 -4
- package/dist/ddl/table-ddl.js.map +1 -1
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +3 -3
- package/dist/exec/executable.js.map +1 -1
- package/dist/exec/queryable.d.ts +160 -160
- package/dist/exec/queryable.js +119 -119
- package/dist/exec/queryable.js.map +1 -1
- package/dist/exec/search-parser.d.ts +37 -37
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr.d.ts +257 -257
- package/dist/expr/expr.js +265 -265
- package/dist/expr/expr.js.map +1 -1
- package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
- package/dist/query-builder/base/expr-renderer-base.js +2 -2
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
- package/dist/query-builder/base/query-builder-base.d.ts +26 -26
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +22 -22
- package/dist/query-builder/base/query-builder-base.js.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/schema/factory/column-builder.d.ts +79 -79
- package/dist/schema/factory/column-builder.js +42 -42
- package/dist/schema/factory/index-builder.d.ts +39 -39
- package/dist/schema/factory/index-builder.js +26 -26
- package/dist/schema/factory/relation-builder.d.ts +99 -99
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +38 -38
- package/dist/schema/procedure-builder.d.ts +49 -49
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +33 -33
- package/dist/schema/table-builder.d.ts +59 -59
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +43 -43
- package/dist/schema/view-builder.d.ts +49 -49
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +32 -32
- package/dist/types/column.d.ts +22 -22
- package/dist/types/column.js +1 -1
- package/dist/types/column.js.map +1 -1
- package/dist/types/db.d.ts +40 -40
- package/dist/types/expr.d.ts +59 -59
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/query-def.d.ts +44 -44
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +3 -3
- package/dist/utils/result-parser.js.map +1 -1
- package/package.json +5 -5
- package/src/create-db-context.ts +20 -20
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +259 -259
- package/src/ddl/relation-ddl.ts +89 -89
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +189 -189
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +2033 -2033
- package/src/exec/search-parser.ts +57 -57
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +2140 -2140
- package/src/query-builder/base/expr-renderer-base.ts +237 -237
- package/src/query-builder/base/query-builder-base.ts +213 -213
- package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
- package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
- package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
- package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
- package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
- package/src/query-builder/query-builder.ts +19 -19
- package/src/schema/factory/column-builder.ts +423 -423
- package/src/schema/factory/index-builder.ts +164 -164
- package/src/schema/factory/relation-builder.ts +453 -453
- package/src/schema/procedure-builder.ts +232 -232
- package/src/schema/table-builder.ts +319 -319
- package/src/schema/view-builder.ts +221 -221
- package/src/types/column.ts +188 -188
- package/src/types/db.ts +208 -208
- package/src/types/expr.ts +697 -697
- package/src/types/query-def.ts +513 -513
- package/src/utils/result-parser.ts +458 -458
- package/tests/db-context/create-db-context.spec.ts +224 -0
- package/tests/db-context/define-db-context.spec.ts +68 -0
- package/tests/ddl/basic.expected.ts +341 -0
- package/tests/ddl/basic.spec.ts +714 -0
- package/tests/ddl/column-builder.expected.ts +310 -0
- package/tests/ddl/column-builder.spec.ts +637 -0
- package/tests/ddl/index-builder.expected.ts +38 -0
- package/tests/ddl/index-builder.spec.ts +202 -0
- package/tests/ddl/procedure-builder.expected.ts +52 -0
- package/tests/ddl/procedure-builder.spec.ts +234 -0
- package/tests/ddl/relation-builder.expected.ts +36 -0
- package/tests/ddl/relation-builder.spec.ts +372 -0
- package/tests/ddl/table-builder.expected.ts +113 -0
- package/tests/ddl/table-builder.spec.ts +433 -0
- package/tests/ddl/view-builder.expected.ts +38 -0
- package/tests/ddl/view-builder.spec.ts +176 -0
- package/tests/dml/delete.expected.ts +96 -0
- package/tests/dml/delete.spec.ts +160 -0
- package/tests/dml/insert.expected.ts +192 -0
- package/tests/dml/insert.spec.ts +288 -0
- package/tests/dml/update.expected.ts +176 -0
- package/tests/dml/update.spec.ts +318 -0
- package/tests/dml/upsert.expected.ts +215 -0
- package/tests/dml/upsert.spec.ts +242 -0
- package/tests/errors/queryable-errors.spec.ts +177 -0
- package/tests/escape.spec.ts +100 -0
- package/tests/examples/pivot.expected.ts +211 -0
- package/tests/examples/pivot.spec.ts +533 -0
- package/tests/examples/sampling.expected.ts +69 -0
- package/tests/examples/sampling.spec.ts +104 -0
- package/tests/examples/unpivot.expected.ts +120 -0
- package/tests/examples/unpivot.spec.ts +226 -0
- package/tests/exec/search-parser.spec.ts +283 -0
- package/tests/executable/basic.expected.ts +18 -0
- package/tests/executable/basic.spec.ts +54 -0
- package/tests/expr/comparison.expected.ts +282 -0
- package/tests/expr/comparison.spec.ts +400 -0
- package/tests/expr/conditional.expected.ts +134 -0
- package/tests/expr/conditional.spec.ts +276 -0
- package/tests/expr/date.expected.ts +332 -0
- package/tests/expr/date.spec.ts +526 -0
- package/tests/expr/math.expected.ts +62 -0
- package/tests/expr/math.spec.ts +106 -0
- package/tests/expr/string.expected.ts +218 -0
- package/tests/expr/string.spec.ts +356 -0
- package/tests/expr/utility.expected.ts +147 -0
- package/tests/expr/utility.spec.ts +182 -0
- package/tests/select/basic.expected.ts +322 -0
- package/tests/select/basic.spec.ts +502 -0
- package/tests/select/filter.expected.ts +357 -0
- package/tests/select/filter.spec.ts +1068 -0
- package/tests/select/group.expected.ts +169 -0
- package/tests/select/group.spec.ts +244 -0
- package/tests/select/join.expected.ts +582 -0
- package/tests/select/join.spec.ts +805 -0
- package/tests/select/order.expected.ts +150 -0
- package/tests/select/order.spec.ts +189 -0
- package/tests/select/recursive-cte.expected.ts +244 -0
- package/tests/select/recursive-cte.spec.ts +514 -0
- package/tests/select/result-meta.spec.ts +270 -0
- package/tests/select/subquery.expected.ts +363 -0
- package/tests/select/subquery.spec.ts +537 -0
- package/tests/select/view.expected.ts +155 -0
- package/tests/select/view.spec.ts +235 -0
- package/tests/select/window.expected.ts +345 -0
- package/tests/select/window.spec.ts +618 -0
- package/tests/setup/MockExecutor.ts +18 -0
- package/tests/setup/TestDbContext.ts +59 -0
- package/tests/setup/models/Company.ts +13 -0
- package/tests/setup/models/Employee.ts +10 -0
- package/tests/setup/models/MonthlySales.ts +11 -0
- package/tests/setup/models/Post.ts +16 -0
- package/tests/setup/models/Sales.ts +10 -0
- package/tests/setup/models/User.ts +19 -0
- package/tests/setup/procedure/GetAllUsers.ts +9 -0
- package/tests/setup/procedure/GetUserById.ts +12 -0
- package/tests/setup/test-utils.ts +72 -0
- package/tests/setup/views/ActiveUsers.ts +8 -0
- package/tests/setup/views/UserSummary.ts +11 -0
- package/tests/types/nullable-queryable-record.spec.ts +145 -0
- package/tests/utils/result-parser-perf.spec.ts +210 -0
- package/tests/utils/result-parser.spec.ts +701 -0
- package/docs/expressions.md +0 -172
- package/docs/queries.md +0 -444
- package/docs/schema.md +0 -245
package/src/exec/queryable.ts
CHANGED
|
@@ -1,2033 +1,2033 @@
|
|
|
1
|
-
import { TableBuilder } from "../schema/table-builder";
|
|
2
|
-
import { ViewBuilder } from "../schema/view-builder";
|
|
3
|
-
|
|
4
|
-
import type { DataRecord, ResultMeta } from "../types/db";
|
|
5
|
-
import type {
|
|
6
|
-
DeleteQueryDef,
|
|
7
|
-
InsertIfNotExistsQueryDef,
|
|
8
|
-
InsertIntoQueryDef,
|
|
9
|
-
InsertQueryDef,
|
|
10
|
-
QueryDefObjectName,
|
|
11
|
-
SelectQueryDef,
|
|
12
|
-
SelectQueryDefJoin,
|
|
13
|
-
UpdateQueryDef,
|
|
14
|
-
UpsertQueryDef,
|
|
15
|
-
} from "../types/query-def";
|
|
16
|
-
import type { DbContextBase } from "../types/db-context-def";
|
|
17
|
-
import {
|
|
18
|
-
type ColumnBuilderRecord,
|
|
19
|
-
type DataToColumnBuilderRecord,
|
|
20
|
-
} from "../schema/factory/column-builder";
|
|
21
|
-
import type { ColumnPrimitive, ColumnPrimitiveStr } from "../types/column";
|
|
22
|
-
import type { WhereExprUnit, ExprInput } from "../expr/expr-unit";
|
|
23
|
-
import { ExprUnit } from "../expr/expr-unit";
|
|
24
|
-
import type { Expr } from "../types/expr";
|
|
25
|
-
import { ArgumentError, objClearUndefined } from "@simplysm/core-common";
|
|
26
|
-
import {
|
|
27
|
-
ForeignKeyBuilder,
|
|
28
|
-
ForeignKeyTargetBuilder,
|
|
29
|
-
RelationKeyBuilder,
|
|
30
|
-
RelationKeyTargetBuilder,
|
|
31
|
-
} from "../schema/factory/relation-builder";
|
|
32
|
-
import { parseSearchQuery } from "./search-parser";
|
|
33
|
-
import { expr } from "../expr/expr";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* JOIN
|
|
37
|
-
*
|
|
38
|
-
* join/joinSingle
|
|
39
|
-
*/
|
|
40
|
-
class JoinQueryable {
|
|
41
|
-
constructor(
|
|
42
|
-
private readonly _db: DbContextBase,
|
|
43
|
-
private readonly _joinAlias: string,
|
|
44
|
-
) {}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @param table -
|
|
50
|
-
* @returns
|
|
51
|
-
*/
|
|
52
|
-
from<T extends TableBuilder<any, any>>(table: T): Queryable<T["$infer"], T> {
|
|
53
|
-
return queryable(this._db, table, this._joinAlias)();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* @param columns -
|
|
60
|
-
* @returns
|
|
61
|
-
*/
|
|
62
|
-
select<R extends DataRecord>(columns: QueryableRecord<R>): Queryable<R, never> {
|
|
63
|
-
return new Queryable({
|
|
64
|
-
db: this._db,
|
|
65
|
-
as: this._joinAlias,
|
|
66
|
-
columns,
|
|
67
|
-
isCustomColumns: true,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* @param queries -
|
|
75
|
-
* @returns UNION
|
|
76
|
-
* @throws 2
|
|
77
|
-
*/
|
|
78
|
-
union<TData extends DataRecord>(...queries: Queryable<TData, any>[]): Queryable<TData, never> {
|
|
79
|
-
if (queries.length < 2) {
|
|
80
|
-
throw new ArgumentError("union
|
|
81
|
-
provided: queries.length,
|
|
82
|
-
minimum: 2,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const first = queries[0];
|
|
87
|
-
|
|
88
|
-
return new Queryable({
|
|
89
|
-
db: first.meta.db,
|
|
90
|
-
from: queries, // Queryable[]
|
|
91
|
-
as: this._joinAlias,
|
|
92
|
-
columns: transformColumnsAlias(first.meta.columns, this._joinAlias, ""),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* recursive()
|
|
101
|
-
*
|
|
102
|
-
* @template TBaseData -
|
|
103
|
-
*/
|
|
104
|
-
class RecursiveQueryable<TBaseData extends DataRecord> {
|
|
105
|
-
constructor(
|
|
106
|
-
private readonly _baseQr: Queryable<TBaseData, any>,
|
|
107
|
-
private readonly _cteName: string,
|
|
108
|
-
) {}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* @param table -
|
|
114
|
-
* @returns self
|
|
115
|
-
*/
|
|
116
|
-
from<T extends TableBuilder<any, any>>(
|
|
117
|
-
table: T,
|
|
118
|
-
): Queryable<T["$infer"] & { self?: TBaseData[] }, T> {
|
|
119
|
-
const selfAlias = `${this._cteName}.self`;
|
|
120
|
-
|
|
121
|
-
return queryable(this._baseQr.meta.db, table, this._cteName)().join(
|
|
122
|
-
"self",
|
|
123
|
-
() =>
|
|
124
|
-
new Queryable<TBaseData, never>({
|
|
125
|
-
db: this._baseQr.meta.db,
|
|
126
|
-
from: this._cteName,
|
|
127
|
-
as: selfAlias,
|
|
128
|
-
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
129
|
-
isCustomColumns: false,
|
|
130
|
-
}),
|
|
131
|
-
) as any;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* @param columns -
|
|
138
|
-
* @returns self
|
|
139
|
-
*/
|
|
140
|
-
select<R extends DataRecord>(
|
|
141
|
-
columns: QueryableRecord<R>,
|
|
142
|
-
): Queryable<R & { self?: TBaseData[] }, never> {
|
|
143
|
-
const selfAlias = `${this._cteName}.self`;
|
|
144
|
-
|
|
145
|
-
return new Queryable<R, never>({
|
|
146
|
-
db: this._baseQr.meta.db,
|
|
147
|
-
as: this._cteName,
|
|
148
|
-
columns,
|
|
149
|
-
isCustomColumns: true,
|
|
150
|
-
}).join(
|
|
151
|
-
"self",
|
|
152
|
-
() =>
|
|
153
|
-
new Queryable<TBaseData, never>({
|
|
154
|
-
db: this._baseQr.meta.db,
|
|
155
|
-
from: this._cteName,
|
|
156
|
-
as: selfAlias,
|
|
157
|
-
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
158
|
-
isCustomColumns: false,
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* @param queries -
|
|
167
|
-
* @returns
|
|
168
|
-
* @throws 2
|
|
169
|
-
*/
|
|
170
|
-
union<TData extends DataRecord>(
|
|
171
|
-
...queries: Queryable<TData, any>[]
|
|
172
|
-
): Queryable<TData & { self?: TBaseData[] }, never> {
|
|
173
|
-
if (queries.length < 2) {
|
|
174
|
-
throw new ArgumentError("union
|
|
175
|
-
provided: queries.length,
|
|
176
|
-
minimum: 2,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const first = queries[0];
|
|
181
|
-
|
|
182
|
-
const selfAlias = `${this._cteName}.self`;
|
|
183
|
-
|
|
184
|
-
return new Queryable<any, never>({
|
|
185
|
-
db: first.meta.db,
|
|
186
|
-
from: queries, // Queryable[]
|
|
187
|
-
as: this._cteName,
|
|
188
|
-
columns: transformColumnsAlias(first.meta.columns, this._cteName, ""),
|
|
189
|
-
}).join(
|
|
190
|
-
"self",
|
|
191
|
-
() =>
|
|
192
|
-
new Queryable({
|
|
193
|
-
db: this._baseQr.meta.db,
|
|
194
|
-
from: this._cteName,
|
|
195
|
-
as: selfAlias,
|
|
196
|
-
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
197
|
-
isCustomColumns: false,
|
|
198
|
-
}),
|
|
199
|
-
) as any;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* @template TData -
|
|
209
|
-
* @template TFrom -
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* ```typescript
|
|
213
|
-
* //
|
|
214
|
-
* const users = await db.user()
|
|
215
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
216
|
-
* .orderBy((u) => u.name)
|
|
217
|
-
* .result();
|
|
218
|
-
*
|
|
219
|
-
* // JOIN
|
|
220
|
-
* const posts = await db.post()
|
|
221
|
-
* .include((p) => p.user)
|
|
222
|
-
* .result();
|
|
223
|
-
*
|
|
224
|
-
* // INSERT
|
|
225
|
-
* await db.user().insert([{ name: "
|
|
226
|
-
* ```
|
|
227
|
-
*/
|
|
228
|
-
export class Queryable<
|
|
229
|
-
TData extends DataRecord,
|
|
230
|
-
TFrom extends TableBuilder<any, any> | never, //
|
|
231
|
-
> {
|
|
232
|
-
constructor(readonly meta: QueryableMeta<TData>) {}
|
|
233
|
-
|
|
234
|
-
//#region ==========
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* @param fn -
|
|
240
|
-
* @returns
|
|
241
|
-
*
|
|
242
|
-
* @example
|
|
243
|
-
* ```typescript
|
|
244
|
-
* db.user().select((u) => ({
|
|
245
|
-
* userName: u.name,
|
|
246
|
-
* userEmail: u.email,
|
|
247
|
-
* }))
|
|
248
|
-
* ```
|
|
249
|
-
*/
|
|
250
|
-
select<R extends Record<string, any>>(
|
|
251
|
-
fn: (columns: QueryableRecord<TData>) => R,
|
|
252
|
-
): Queryable<UnwrapQueryableRecord<R>, never> {
|
|
253
|
-
if (Array.isArray(this.meta.from)) {
|
|
254
|
-
const newFroms = this.meta.from.map((from) => from.select(fn));
|
|
255
|
-
return new Queryable({
|
|
256
|
-
...this.meta,
|
|
257
|
-
from: newFroms,
|
|
258
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
259
|
-
}) as any;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const newColumns = fn(this.meta.columns);
|
|
263
|
-
|
|
264
|
-
return new Queryable<any, never>({
|
|
265
|
-
...this.meta,
|
|
266
|
-
columns: newColumns,
|
|
267
|
-
isCustomColumns: true,
|
|
268
|
-
}) as any;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* DISTINCT
|
|
273
|
-
*
|
|
274
|
-
* @returns DISTINCT
|
|
275
|
-
*
|
|
276
|
-
* @example
|
|
277
|
-
* ```typescript
|
|
278
|
-
* db.user()
|
|
279
|
-
* .select((u) => ({ name: u.name }))
|
|
280
|
-
* .distinct()
|
|
281
|
-
* ```
|
|
282
|
-
*/
|
|
283
|
-
distinct(): Queryable<TData, never> {
|
|
284
|
-
if (Array.isArray(this.meta.from)) {
|
|
285
|
-
const newFroms = this.meta.from.map((from) => from.distinct());
|
|
286
|
-
return new Queryable({
|
|
287
|
-
...this.meta,
|
|
288
|
-
from: newFroms,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return new Queryable({
|
|
293
|
-
...this.meta,
|
|
294
|
-
distinct: true,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* @returns
|
|
304
|
-
*
|
|
305
|
-
* @example
|
|
306
|
-
* ```typescript
|
|
307
|
-
* await db.connect(async () => {
|
|
308
|
-
* const user = await db.user()
|
|
309
|
-
* .where((u) => [expr.eq(u.id, 1)])
|
|
310
|
-
* .lock()
|
|
311
|
-
* .single();
|
|
312
|
-
* });
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
|
-
lock(): Queryable<TData, TFrom> {
|
|
316
|
-
if (Array.isArray(this.meta.from)) {
|
|
317
|
-
const newFroms = this.meta.from.map((from) => from.lock());
|
|
318
|
-
return new Queryable({
|
|
319
|
-
...this.meta,
|
|
320
|
-
from: newFroms,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return new Queryable({
|
|
325
|
-
...this.meta,
|
|
326
|
-
lock: true,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
//#endregion
|
|
331
|
-
|
|
332
|
-
//#region ==========
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* @param count -
|
|
338
|
-
* @returns TOP
|
|
339
|
-
*
|
|
340
|
-
* @example
|
|
341
|
-
* ```typescript
|
|
342
|
-
* //
|
|
343
|
-
* db.user()
|
|
344
|
-
* .orderBy((u) => u.createdAt, "DESC")
|
|
345
|
-
* .top(10)
|
|
346
|
-
* ```
|
|
347
|
-
*/
|
|
348
|
-
top(count: number): Queryable<TData, TFrom> {
|
|
349
|
-
if (Array.isArray(this.meta.from)) {
|
|
350
|
-
const newFroms = this.meta.from.map((from) => from.top(count));
|
|
351
|
-
return new Queryable({
|
|
352
|
-
...this.meta,
|
|
353
|
-
from: newFroms,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return new Queryable({
|
|
358
|
-
...this.meta,
|
|
359
|
-
top: count,
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* @param skip -
|
|
368
|
-
* @param take -
|
|
369
|
-
* @returns
|
|
370
|
-
* @throws ORDER BY
|
|
371
|
-
*
|
|
372
|
-
* @example
|
|
373
|
-
* ```typescript
|
|
374
|
-
* db.user
|
|
375
|
-
* .orderBy((u) => u.createdAt)
|
|
376
|
-
* .limit(0, 20) //
|
|
377
|
-
* ```
|
|
378
|
-
*/
|
|
379
|
-
limit(skip: number, take: number): Queryable<TData, TFrom> {
|
|
380
|
-
if (Array.isArray(this.meta.from)) {
|
|
381
|
-
const newFroms = this.meta.from.map((from) => from.limit(skip, take));
|
|
382
|
-
return new Queryable({
|
|
383
|
-
...this.meta,
|
|
384
|
-
from: newFroms,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (!this.meta.orderBy) {
|
|
389
|
-
throw new ArgumentError("limit()
|
|
390
|
-
method: "limit",
|
|
391
|
-
required: "orderBy",
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return new Queryable({
|
|
396
|
-
...this.meta,
|
|
397
|
-
limit: [skip, take],
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
//#endregion
|
|
402
|
-
|
|
403
|
-
//#region ==========
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* @param fn -
|
|
409
|
-
* @param orderBy -
|
|
410
|
-
* @returns
|
|
411
|
-
*
|
|
412
|
-
* @example
|
|
413
|
-
* ```typescript
|
|
414
|
-
* db.user
|
|
415
|
-
* .orderBy((u) => u.name) // 이름 ASC
|
|
416
|
-
* .orderBy((u) => u.age, "DESC") // 나이 DESC
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
orderBy(
|
|
420
|
-
fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>,
|
|
421
|
-
orderBy?: "ASC" | "DESC",
|
|
422
|
-
): Queryable<TData, TFrom> {
|
|
423
|
-
if (Array.isArray(this.meta.from)) {
|
|
424
|
-
const newFroms = this.meta.from.map((from) => from.orderBy(fn, orderBy));
|
|
425
|
-
return new Queryable({
|
|
426
|
-
...this.meta,
|
|
427
|
-
from: newFroms,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const column = fn(this.meta.columns);
|
|
432
|
-
|
|
433
|
-
return new Queryable({
|
|
434
|
-
...this.meta,
|
|
435
|
-
orderBy: [...(this.meta.orderBy ?? []), [column, orderBy]],
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
//#endregion
|
|
440
|
-
|
|
441
|
-
//#region ========== 검색 - WHERE ==========
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* WHERE
|
|
445
|
-
*
|
|
446
|
-
* @param predicate -
|
|
447
|
-
* @returns 조건이 추가된 Queryable
|
|
448
|
-
*
|
|
449
|
-
* @example
|
|
450
|
-
* ```typescript
|
|
451
|
-
* db.user
|
|
452
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
453
|
-
* .where((u) => [expr.gte(u.age, 18)])
|
|
454
|
-
* ```
|
|
455
|
-
*/
|
|
456
|
-
where(predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[]): Queryable<TData, TFrom> {
|
|
457
|
-
if (Array.isArray(this.meta.from)) {
|
|
458
|
-
const newFroms = this.meta.from.map((from) => from.where(predicate));
|
|
459
|
-
return new Queryable({
|
|
460
|
-
...this.meta,
|
|
461
|
-
from: newFroms,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const conditions = predicate(this.meta.columns);
|
|
466
|
-
|
|
467
|
-
return new Queryable({
|
|
468
|
-
...this.meta,
|
|
469
|
-
where: [...(this.meta.where ?? []), ...conditions],
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* 텍스트 검색을 수행
|
|
475
|
-
*
|
|
476
|
-
* 검색 문법은 {@link parseSearchQuery}를 참조
|
|
477
|
-
* - 공백으로 구분된 단어는 OR
|
|
478
|
-
* - `+`로 시작하는 단어는
|
|
479
|
-
* - `-`로 시작하는 단어는
|
|
480
|
-
*
|
|
481
|
-
* @param fn - 검색 대상
|
|
482
|
-
* @param searchText - 검색 텍스트
|
|
483
|
-
* @returns 검색 조건이 추가된 Queryable
|
|
484
|
-
*
|
|
485
|
-
* @example
|
|
486
|
-
* ```typescript
|
|
487
|
-
* db.user()
|
|
488
|
-
* .search((u) => [u.name, u.email], "
|
|
489
|
-
* ```
|
|
490
|
-
*/
|
|
491
|
-
search(
|
|
492
|
-
fn: (columns: QueryableRecord<TData>) => ExprUnit<string | undefined>[],
|
|
493
|
-
searchText: string,
|
|
494
|
-
): Queryable<TData, TFrom> {
|
|
495
|
-
if (Array.isArray(this.meta.from)) {
|
|
496
|
-
const newFroms = this.meta.from.map((from) => from.search(fn, searchText));
|
|
497
|
-
return new Queryable({
|
|
498
|
-
...this.meta,
|
|
499
|
-
from: newFroms,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (searchText.trim() === "") {
|
|
504
|
-
return this;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const columns = fn(this.meta.columns);
|
|
508
|
-
const parsed = parseSearchQuery(searchText);
|
|
509
|
-
|
|
510
|
-
const conditions: WhereExprUnit[] = [];
|
|
511
|
-
|
|
512
|
-
// OR
|
|
513
|
-
if (parsed.or.length === 1) {
|
|
514
|
-
const pattern = parsed.or[0];
|
|
515
|
-
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
516
|
-
conditions.push(expr.or(columnMatches));
|
|
517
|
-
} else if (parsed.or.length > 1) {
|
|
518
|
-
const orConditions = parsed.or.map((pattern) => {
|
|
519
|
-
const columnMatches = columns.map((col) =>
|
|
520
|
-
expr.like(expr.lower(col), pattern.toLowerCase()),
|
|
521
|
-
);
|
|
522
|
-
return expr.or(columnMatches);
|
|
523
|
-
});
|
|
524
|
-
conditions.push(expr.or(orConditions));
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// MUST
|
|
528
|
-
for (const pattern of parsed.must) {
|
|
529
|
-
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
530
|
-
conditions.push(expr.or(columnMatches));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// NOT
|
|
534
|
-
for (const pattern of parsed.not) {
|
|
535
|
-
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
536
|
-
conditions.push(expr.not(expr.or(columnMatches)));
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (conditions.length === 0) {
|
|
540
|
-
return this;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return this.where(() => [expr.and(conditions)]);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
//#endregion
|
|
547
|
-
|
|
548
|
-
//#region ========== 그룹 - GROUP BY / HAVING ==========
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* GROUP BY 절을
|
|
552
|
-
*
|
|
553
|
-
* @param fn -
|
|
554
|
-
* @returns GROUP BY
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* ```typescript
|
|
558
|
-
* db.order()
|
|
559
|
-
* .select((o) => ({
|
|
560
|
-
* userId: o.userId,
|
|
561
|
-
* totalAmount: expr.sum(o.amount),
|
|
562
|
-
* }))
|
|
563
|
-
* .groupBy((o) => [o.userId])
|
|
564
|
-
* ```
|
|
565
|
-
*/
|
|
566
|
-
groupBy(
|
|
567
|
-
fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>[],
|
|
568
|
-
): Queryable<TData, never> {
|
|
569
|
-
if (Array.isArray(this.meta.from)) {
|
|
570
|
-
const newFroms = this.meta.from.map((from) => from.groupBy(fn));
|
|
571
|
-
return new Queryable({
|
|
572
|
-
...this.meta,
|
|
573
|
-
from: newFroms,
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const groupBy = fn(this.meta.columns);
|
|
578
|
-
|
|
579
|
-
return new Queryable({ ...this.meta, groupBy });
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* HAVING 절을
|
|
584
|
-
*
|
|
585
|
-
* @param predicate -
|
|
586
|
-
* @returns HAVING이
|
|
587
|
-
*
|
|
588
|
-
* @example
|
|
589
|
-
* ```typescript
|
|
590
|
-
* db.order()
|
|
591
|
-
* .select((o) => ({
|
|
592
|
-
* userId: o.userId,
|
|
593
|
-
* totalAmount: expr.sum(o.amount),
|
|
594
|
-
* }))
|
|
595
|
-
* .groupBy((o) => [o.userId])
|
|
596
|
-
* .having((o) => [expr.gte(o.totalAmount, 10000)])
|
|
597
|
-
* ```
|
|
598
|
-
*/
|
|
599
|
-
having(predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[]): Queryable<TData, never> {
|
|
600
|
-
if (Array.isArray(this.meta.from)) {
|
|
601
|
-
const newFroms = this.meta.from.map((from) => from.having(predicate));
|
|
602
|
-
return new Queryable({
|
|
603
|
-
...this.meta,
|
|
604
|
-
from: newFroms,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const conditions = predicate(this.meta.columns);
|
|
609
|
-
|
|
610
|
-
return new Queryable({
|
|
611
|
-
...this.meta,
|
|
612
|
-
having: [...(this.meta.having ?? []), ...conditions],
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
//#endregion
|
|
617
|
-
|
|
618
|
-
//#region ==========
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* 1:N 관계의 LEFT OUTER JOIN을 수행 (배열로
|
|
622
|
-
*
|
|
623
|
-
* @param as -
|
|
624
|
-
* @param fwd -
|
|
625
|
-
* @returns
|
|
626
|
-
*
|
|
627
|
-
* @example
|
|
628
|
-
* ```typescript
|
|
629
|
-
* db.user()
|
|
630
|
-
* .join("posts", (qr, u) =>
|
|
631
|
-
* qr.from(Post)
|
|
632
|
-
* .where((p) => [expr.eq(p.userId, u.id)])
|
|
633
|
-
* )
|
|
634
|
-
* //
|
|
635
|
-
* ```
|
|
636
|
-
*/
|
|
637
|
-
join<A extends string, R extends DataRecord>(
|
|
638
|
-
as: A,
|
|
639
|
-
fwd: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>,
|
|
640
|
-
): Queryable<TData & { [K in A]?: R[] }, TFrom> {
|
|
641
|
-
if (Array.isArray(this.meta.from)) {
|
|
642
|
-
const newFroms = this.meta.from.map((from) => from.join(as, fwd));
|
|
643
|
-
return new Queryable({
|
|
644
|
-
...this.meta,
|
|
645
|
-
from: newFroms,
|
|
646
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// 1. join alias
|
|
651
|
-
const joinAlias = `${this.meta.as}.${as}`;
|
|
652
|
-
|
|
653
|
-
// 2. target → Queryable
|
|
654
|
-
const joinQr = new JoinQueryable(this.meta.db, joinAlias);
|
|
655
|
-
|
|
656
|
-
// 3. fwd 실행 (where 등
|
|
657
|
-
const resultQr = fwd(joinQr, this.meta.columns);
|
|
658
|
-
|
|
659
|
-
// 4. 새 columns에 join
|
|
660
|
-
const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
|
|
661
|
-
|
|
662
|
-
return new Queryable({
|
|
663
|
-
...this.meta,
|
|
664
|
-
columns: {
|
|
665
|
-
...this.meta.columns,
|
|
666
|
-
[as]: [joinColumns],
|
|
667
|
-
} as QueryableRecord<any>,
|
|
668
|
-
isCustomColumns: true,
|
|
669
|
-
joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: false }],
|
|
670
|
-
}) as any;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* N:1 또는 1:1 관계의 LEFT OUTER JOIN을 수행 (단일 객체로
|
|
675
|
-
*
|
|
676
|
-
* @param as -
|
|
677
|
-
* @param fwd -
|
|
678
|
-
* @returns
|
|
679
|
-
*
|
|
680
|
-
* @example
|
|
681
|
-
* ```typescript
|
|
682
|
-
* db.post()
|
|
683
|
-
* .joinSingle("user", (qr, p) =>
|
|
684
|
-
* qr.from(User)
|
|
685
|
-
* .where((u) => [expr.eq(u.id, p.userId)])
|
|
686
|
-
* )
|
|
687
|
-
* //
|
|
688
|
-
* ```
|
|
689
|
-
*/
|
|
690
|
-
joinSingle<A extends string, R extends DataRecord>(
|
|
691
|
-
as: A,
|
|
692
|
-
fwd: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>,
|
|
693
|
-
): Queryable<
|
|
694
|
-
{ [K in keyof TData as K extends A ? never : K]: TData[K] } & { [K in A]?: R },
|
|
695
|
-
TFrom
|
|
696
|
-
> {
|
|
697
|
-
if (Array.isArray(this.meta.from)) {
|
|
698
|
-
const newFroms = this.meta.from.map((from) => from.joinSingle(as, fwd));
|
|
699
|
-
return new Queryable({
|
|
700
|
-
...this.meta,
|
|
701
|
-
from: newFroms,
|
|
702
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// 1. join alias
|
|
707
|
-
const joinAlias = `${this.meta.as}.${as}`;
|
|
708
|
-
|
|
709
|
-
// 2. target → Queryable
|
|
710
|
-
const joinQr = new JoinQueryable(this.meta.db, joinAlias);
|
|
711
|
-
|
|
712
|
-
// 3. fwd 실행 (where 등
|
|
713
|
-
const resultQr = fwd(joinQr, this.meta.columns);
|
|
714
|
-
|
|
715
|
-
// 4. 새 columns에 join
|
|
716
|
-
const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
|
|
717
|
-
|
|
718
|
-
return new Queryable({
|
|
719
|
-
...this.meta,
|
|
720
|
-
columns: {
|
|
721
|
-
...this.meta.columns,
|
|
722
|
-
[as]: joinColumns,
|
|
723
|
-
} as QueryableRecord<any>,
|
|
724
|
-
isCustomColumns: true,
|
|
725
|
-
joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: true }],
|
|
726
|
-
}) as any;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
//#endregion
|
|
730
|
-
|
|
731
|
-
//#region ==========
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* 관계된
|
|
735
|
-
* TableBuilder에 정의된 FK/FKT 관계를 기반으로 동작합니다.
|
|
736
|
-
*
|
|
737
|
-
* @param fn - 포함할 관계를 선택하는
|
|
738
|
-
* @returns JOIN이 추가된 Queryable
|
|
739
|
-
* @throws 관계가 정의되지 않은 경우 에러
|
|
740
|
-
*
|
|
741
|
-
* @example
|
|
742
|
-
* ```typescript
|
|
743
|
-
* // 단일
|
|
744
|
-
* db.post.include((p) => p.user)
|
|
745
|
-
*
|
|
746
|
-
* // 중첩
|
|
747
|
-
* db.post.include((p) => p.user.company)
|
|
748
|
-
*
|
|
749
|
-
* // 다중
|
|
750
|
-
* db.user
|
|
751
|
-
* .include((u) => u.company)
|
|
752
|
-
* .include((u) => u.posts)
|
|
753
|
-
* ```
|
|
754
|
-
*/
|
|
755
|
-
include(fn: (item: PathProxy<TData>) => PathProxy<any>): Queryable<TData, TFrom> {
|
|
756
|
-
if (Array.isArray(this.meta.from)) {
|
|
757
|
-
const newFroms = this.meta.from.map((from) => from.include(fn));
|
|
758
|
-
return new Queryable({
|
|
759
|
-
...this.meta,
|
|
760
|
-
from: newFroms,
|
|
761
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const proxy = createPathProxy<TData>();
|
|
766
|
-
const result = fn(proxy);
|
|
767
|
-
const relationChain = result[PATH_SYMBOL].join(".");
|
|
768
|
-
|
|
769
|
-
return this._include(relationChain);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
private _include(relationChain: string): Queryable<TData, TFrom> {
|
|
773
|
-
const relationNames = relationChain.split(".");
|
|
774
|
-
|
|
775
|
-
let result: Queryable<any, any> = this;
|
|
776
|
-
let currentTable = this.meta.from;
|
|
777
|
-
const chainParts: string[] = [];
|
|
778
|
-
|
|
779
|
-
for (const relationName of relationNames) {
|
|
780
|
-
if (!(currentTable instanceof TableBuilder)) {
|
|
781
|
-
throw new Error("include()
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const parentChain = chainParts.join(".");
|
|
785
|
-
chainParts.push(relationName);
|
|
786
|
-
|
|
787
|
-
// 이미 JOIN된 경우 중복
|
|
788
|
-
const targetAlias = `${result.meta.as}.${chainParts.join(".")}`;
|
|
789
|
-
const existingJoin = result.meta.joins?.find((j) => j.queryable.meta.as === targetAlias);
|
|
790
|
-
if (existingJoin) {
|
|
791
|
-
// 기존 JOIN의
|
|
792
|
-
const existingFrom = existingJoin.queryable.meta.from;
|
|
793
|
-
if (existingFrom instanceof TableBuilder) {
|
|
794
|
-
currentTable = existingFrom;
|
|
795
|
-
}
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const relationDef = currentTable.meta.relations?.[relationName];
|
|
800
|
-
if (relationDef == null) {
|
|
801
|
-
throw new Error(
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (relationDef instanceof ForeignKeyBuilder || relationDef instanceof RelationKeyBuilder) {
|
|
805
|
-
// FK/RelationKey (N:1): Post.user → User
|
|
806
|
-
//
|
|
807
|
-
const targetTable = relationDef.meta.targetFn();
|
|
808
|
-
const fkColKeys = relationDef.meta.columns;
|
|
809
|
-
const targetPkColKeys = getMatchedPrimaryKeys(fkColKeys, targetTable);
|
|
810
|
-
|
|
811
|
-
result = result.joinSingle(chainParts.join("."), (joinQr, parentCols) => {
|
|
812
|
-
const qr = joinQr.from(targetTable);
|
|
813
|
-
|
|
814
|
-
// FKT join은 배열로 저장되므로 배열인 경우 첫 번째 요소 사용
|
|
815
|
-
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
816
|
-
const srcCols = (
|
|
817
|
-
Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw
|
|
818
|
-
) as QueryableRecord<any>;
|
|
819
|
-
const conditions: WhereExprUnit[] = [];
|
|
820
|
-
|
|
821
|
-
for (let i = 0; i < fkColKeys.length; i++) {
|
|
822
|
-
const fkCol = srcCols[fkColKeys[i]];
|
|
823
|
-
const pkCol = qr.meta.columns[targetPkColKeys[i]] as ExprUnit<ColumnPrimitive>;
|
|
824
|
-
|
|
825
|
-
conditions.push(expr.eq(pkCol, fkCol));
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
return qr.where(() => conditions);
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
currentTable = targetTable;
|
|
832
|
-
} else if (
|
|
833
|
-
relationDef instanceof ForeignKeyTargetBuilder ||
|
|
834
|
-
relationDef instanceof RelationKeyTargetBuilder
|
|
835
|
-
) {
|
|
836
|
-
// FKT/RelationKeyTarget (1:N 또는 1:1): User.posts → Post[]
|
|
837
|
-
//
|
|
838
|
-
const targetTable = relationDef.meta.targetTableFn();
|
|
839
|
-
const fkRelName = relationDef.meta.relationName;
|
|
840
|
-
const sourceFk = targetTable.meta.relations?.[fkRelName];
|
|
841
|
-
if (!(sourceFk instanceof ForeignKeyBuilder) && !(sourceFk instanceof RelationKeyBuilder)) {
|
|
842
|
-
throw new Error(
|
|
843
|
-
`'${relationName}'이 참조하는 '${fkRelName}'이(가) ` +
|
|
844
|
-
`${targetTable.meta.name}
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
const sourceTable = targetTable;
|
|
848
|
-
const isSingle: boolean = relationDef.meta.isSingle ?? false;
|
|
849
|
-
|
|
850
|
-
const fkColKeys = sourceFk.meta.columns;
|
|
851
|
-
const pkColKeys = getMatchedPrimaryKeys(fkColKeys, currentTable);
|
|
852
|
-
|
|
853
|
-
const buildJoin = (joinQr: JoinQueryable, parentCols: QueryableRecord<DataRecord>) => {
|
|
854
|
-
const qr = joinQr.from(sourceTable);
|
|
855
|
-
|
|
856
|
-
// FKT join은 배열로 저장되므로 배열인 경우 첫 번째 요소 사용
|
|
857
|
-
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
858
|
-
const srcCols = (
|
|
859
|
-
Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw
|
|
860
|
-
) as QueryableRecord<any>;
|
|
861
|
-
const conditions: WhereExprUnit[] = [];
|
|
862
|
-
|
|
863
|
-
for (let i = 0; i < fkColKeys.length; i++) {
|
|
864
|
-
const pkCol = srcCols[pkColKeys[i]] as ExprUnit<ColumnPrimitive>;
|
|
865
|
-
const fkCol = qr.meta.columns[fkColKeys[i]] as ExprUnit<ColumnPrimitive>;
|
|
866
|
-
|
|
867
|
-
conditions.push(expr.eq(fkCol, pkCol));
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
return qr.where(() => conditions);
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
result = isSingle
|
|
874
|
-
? result.joinSingle(chainParts.join("."), buildJoin)
|
|
875
|
-
: result.join(chainParts.join("."), buildJoin);
|
|
876
|
-
|
|
877
|
-
currentTable = sourceTable;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
return result as Queryable<TData, TFrom>;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
//#endregion
|
|
885
|
-
|
|
886
|
-
//#region ==========
|
|
887
|
-
|
|
888
|
-
/**
|
|
889
|
-
* 현재 Queryable을
|
|
890
|
-
*
|
|
891
|
-
* distinct() 또는 groupBy() 후 count() 사용 시 필요
|
|
892
|
-
*
|
|
893
|
-
* @returns
|
|
894
|
-
*
|
|
895
|
-
* @example
|
|
896
|
-
* ```typescript
|
|
897
|
-
* // DISTINCT 후 카운트
|
|
898
|
-
* const count = await db.user()
|
|
899
|
-
* .select((u) => ({ name: u.name }))
|
|
900
|
-
* .distinct()
|
|
901
|
-
* .wrap()
|
|
902
|
-
* .count();
|
|
903
|
-
* ```
|
|
904
|
-
*/
|
|
905
|
-
wrap(): Queryable<TData, never> {
|
|
906
|
-
// 현재 Queryable을
|
|
907
|
-
const wrapAlias = this.meta.db.getNextAlias();
|
|
908
|
-
return new Queryable({
|
|
909
|
-
db: this.meta.db,
|
|
910
|
-
from: this,
|
|
911
|
-
as: wrapAlias,
|
|
912
|
-
columns: transformColumnsAlias<TData>(this.meta.columns, wrapAlias, ""),
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/**
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
* @param queries -
|
|
920
|
-
* @returns UNION
|
|
921
|
-
* @throws 2
|
|
922
|
-
*
|
|
923
|
-
* @example
|
|
924
|
-
* ```typescript
|
|
925
|
-
* const combined = Queryable.union(
|
|
926
|
-
* db.user().where((u) => [expr.eq(u.type, "admin")]),
|
|
927
|
-
* db.user().where((u) => [expr.eq(u.type, "manager")]),
|
|
928
|
-
* );
|
|
929
|
-
* ```
|
|
930
|
-
*/
|
|
931
|
-
static union<TData extends DataRecord>(
|
|
932
|
-
...queries: Queryable<TData, any>[]
|
|
933
|
-
): Queryable<TData, never> {
|
|
934
|
-
if (queries.length < 2) {
|
|
935
|
-
throw new ArgumentError("union
|
|
936
|
-
provided: queries.length,
|
|
937
|
-
minimum: 2,
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const first = queries[0];
|
|
942
|
-
const unionAlias = first.meta.db.getNextAlias();
|
|
943
|
-
return new Queryable({
|
|
944
|
-
db: first.meta.db,
|
|
945
|
-
from: queries, // Queryable[]
|
|
946
|
-
as: unionAlias,
|
|
947
|
-
columns: transformColumnsAlias(first.meta.columns, unionAlias, ""),
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
//#endregion
|
|
952
|
-
|
|
953
|
-
//#region ==========
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
* 계층
|
|
959
|
-
*
|
|
960
|
-
* @param fwd -
|
|
961
|
-
* @returns
|
|
962
|
-
*
|
|
963
|
-
* @example
|
|
964
|
-
* ```typescript
|
|
965
|
-
* // 조직도 계층 조회
|
|
966
|
-
* db.employee()
|
|
967
|
-
* .where((e) => [expr.null(e.managerId)]) // 루트 노드
|
|
968
|
-
* .recursive((cte) =>
|
|
969
|
-
* cte.from(Employee)
|
|
970
|
-
* .where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
971
|
-
* )
|
|
972
|
-
* ```
|
|
973
|
-
*/
|
|
974
|
-
recursive(
|
|
975
|
-
fwd: (qr: RecursiveQueryable<TData>) => Queryable<TData, any>,
|
|
976
|
-
): Queryable<TData, never> {
|
|
977
|
-
if (Array.isArray(this.meta.from)) {
|
|
978
|
-
const newFroms = this.meta.from.map((from) => from.recursive(fwd));
|
|
979
|
-
return new Queryable({
|
|
980
|
-
...this.meta,
|
|
981
|
-
from: newFroms,
|
|
982
|
-
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
// 동적 CTE 이름
|
|
986
|
-
const cteName = this.meta.db.getNextAlias();
|
|
987
|
-
|
|
988
|
-
// 2. target → Queryable
|
|
989
|
-
const cteQr = new RecursiveQueryable(this, cteName);
|
|
990
|
-
|
|
991
|
-
// 3. fwd 실행 (where 등
|
|
992
|
-
const resultQr = fwd(cteQr);
|
|
993
|
-
|
|
994
|
-
return new Queryable({
|
|
995
|
-
db: this.meta.db,
|
|
996
|
-
as: this.meta.as,
|
|
997
|
-
from: cteName,
|
|
998
|
-
columns: transformColumnsAlias(this.meta.columns, this.meta.as, ""),
|
|
999
|
-
with: {
|
|
1000
|
-
name: cteName,
|
|
1001
|
-
base: this as any, //
|
|
1002
|
-
recursive: resultQr,
|
|
1003
|
-
},
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
//#endregion
|
|
1008
|
-
|
|
1009
|
-
//#region ========== [
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* SELECT
|
|
1013
|
-
*
|
|
1014
|
-
* @returns
|
|
1015
|
-
*
|
|
1016
|
-
* @example
|
|
1017
|
-
* ```typescript
|
|
1018
|
-
* const users = await db.user()
|
|
1019
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1020
|
-
* .result();
|
|
1021
|
-
* ```
|
|
1022
|
-
*/
|
|
1023
|
-
async result(): Promise<TData[]> {
|
|
1024
|
-
const results = await this.meta.db.executeDefs<TData>(
|
|
1025
|
-
[this.getSelectQueryDef()],
|
|
1026
|
-
[this.getResultMeta()],
|
|
1027
|
-
);
|
|
1028
|
-
return results[0];
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* 단일 결과를
|
|
1033
|
-
*
|
|
1034
|
-
* @returns 단일
|
|
1035
|
-
* @throws 2개 이상의 결과가 반환된 경우
|
|
1036
|
-
*
|
|
1037
|
-
* @example
|
|
1038
|
-
* ```typescript
|
|
1039
|
-
* const user = await db.user()
|
|
1040
|
-
* .where((u) => [expr.eq(u.id, 1)])
|
|
1041
|
-
* .single();
|
|
1042
|
-
* ```
|
|
1043
|
-
*/
|
|
1044
|
-
async single(): Promise<TData | undefined> {
|
|
1045
|
-
const result = await this.top(2).result();
|
|
1046
|
-
if (result.length > 1) {
|
|
1047
|
-
throw new ArgumentError("
|
|
1048
|
-
table: this._getSourceName(),
|
|
1049
|
-
resultCount: result.length,
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
return result[0];
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/**
|
|
1056
|
-
*
|
|
1057
|
-
*/
|
|
1058
|
-
private _getSourceName(): string {
|
|
1059
|
-
const from = this.meta.from;
|
|
1060
|
-
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
1061
|
-
return from.meta.name;
|
|
1062
|
-
}
|
|
1063
|
-
if (typeof from === "string") {
|
|
1064
|
-
return from;
|
|
1065
|
-
}
|
|
1066
|
-
return this.meta.as;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* 첫 번째 결과를
|
|
1071
|
-
*
|
|
1072
|
-
* @returns 첫 번째
|
|
1073
|
-
*
|
|
1074
|
-
* @example
|
|
1075
|
-
* ```typescript
|
|
1076
|
-
* const latestUser = await db.user()
|
|
1077
|
-
* .orderBy((u) => u.createdAt, "DESC")
|
|
1078
|
-
* .first();
|
|
1079
|
-
* ```
|
|
1080
|
-
*/
|
|
1081
|
-
async first(): Promise<TData | undefined> {
|
|
1082
|
-
const results = await this.top(1).result();
|
|
1083
|
-
return results[0];
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
/**
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
* @param fwd - 카운트할
|
|
1090
|
-
* @returns
|
|
1091
|
-
* @throws distinct() 또는 groupBy() 후 직접 호출 시 에러 (wrap() 필요)
|
|
1092
|
-
*
|
|
1093
|
-
* @example
|
|
1094
|
-
* ```typescript
|
|
1095
|
-
* const count = await db.user()
|
|
1096
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1097
|
-
* .count();
|
|
1098
|
-
* ```
|
|
1099
|
-
*/
|
|
1100
|
-
async count(fwd?: (cols: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>): Promise<number> {
|
|
1101
|
-
if (this.meta.distinct) {
|
|
1102
|
-
throw new Error("
|
|
1103
|
-
}
|
|
1104
|
-
if (this.meta.groupBy) {
|
|
1105
|
-
throw new Error("
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
const countQr = fwd
|
|
1109
|
-
? this.select((c) => ({ cnt: expr.count(fwd(c)) }))
|
|
1110
|
-
: this.select(() => ({ cnt: expr.count() }));
|
|
1111
|
-
|
|
1112
|
-
const result = await countQr.single();
|
|
1113
|
-
|
|
1114
|
-
return result?.cnt ?? 0;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
/**
|
|
1118
|
-
* 조건에 맞는
|
|
1119
|
-
*
|
|
1120
|
-
* @returns 존재하면 true, 없으면 false
|
|
1121
|
-
*
|
|
1122
|
-
* @example
|
|
1123
|
-
* ```typescript
|
|
1124
|
-
* const hasAdmin = await db.user()
|
|
1125
|
-
* .where((u) => [expr.eq(u.role, "admin")])
|
|
1126
|
-
* .exists();
|
|
1127
|
-
* ```
|
|
1128
|
-
*/
|
|
1129
|
-
async exists(): Promise<boolean> {
|
|
1130
|
-
const count = await this.count();
|
|
1131
|
-
return count > 0;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
getSelectQueryDef(): SelectQueryDef {
|
|
1135
|
-
return objClearUndefined({
|
|
1136
|
-
type: "select",
|
|
1137
|
-
from: this._buildFromDef(),
|
|
1138
|
-
as: this.meta.as,
|
|
1139
|
-
select: this.meta.isCustomColumns ? this._buildSelectDef(this.meta.columns, "") : undefined,
|
|
1140
|
-
distinct: this.meta.distinct,
|
|
1141
|
-
top: this.meta.top,
|
|
1142
|
-
lock: this.meta.lock,
|
|
1143
|
-
where: this.meta.where?.map((w) => w.expr),
|
|
1144
|
-
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1145
|
-
orderBy: this.meta.orderBy?.map((o) => (o[1] ? [o[0].expr, o[1]] : [o[0].expr])),
|
|
1146
|
-
limit: this.meta.limit,
|
|
1147
|
-
groupBy: this.meta.groupBy?.map((g) => g.expr),
|
|
1148
|
-
having: this.meta.having?.map((w) => w.expr),
|
|
1149
|
-
with: this.meta.with
|
|
1150
|
-
? {
|
|
1151
|
-
name: this.meta.with.name,
|
|
1152
|
-
base: this.meta.with.base.getSelectQueryDef(),
|
|
1153
|
-
recursive: this.meta.with.recursive.getSelectQueryDef(),
|
|
1154
|
-
}
|
|
1155
|
-
: undefined,
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
private _buildFromDef():
|
|
1160
|
-
| QueryDefObjectName
|
|
1161
|
-
| SelectQueryDef
|
|
1162
|
-
| SelectQueryDef[]
|
|
1163
|
-
| string
|
|
1164
|
-
| undefined {
|
|
1165
|
-
const from = this.meta.from;
|
|
1166
|
-
|
|
1167
|
-
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
1168
|
-
return this.meta.db.getQueryDefObjectName(from);
|
|
1169
|
-
} else if (from instanceof Queryable) {
|
|
1170
|
-
return from.getSelectQueryDef();
|
|
1171
|
-
} else if (Array.isArray(from)) {
|
|
1172
|
-
return from.map((qr) => qr.getSelectQueryDef());
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
return from;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
private _buildSelectDef(
|
|
1179
|
-
columns: QueryableRecord<any> | QueryableWriteRecord<any>,
|
|
1180
|
-
prefix: string,
|
|
1181
|
-
): Record<string, Expr> {
|
|
1182
|
-
const result: Record<string, Expr> = {};
|
|
1183
|
-
|
|
1184
|
-
for (const [key, val] of Object.entries(columns)) {
|
|
1185
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1186
|
-
|
|
1187
|
-
if (val instanceof ExprUnit) {
|
|
1188
|
-
result[fullKey] = val.expr;
|
|
1189
|
-
} else if (Array.isArray(val)) {
|
|
1190
|
-
if (val.length > 0) {
|
|
1191
|
-
Object.assign(result, this._buildSelectDef(val[0], fullKey));
|
|
1192
|
-
}
|
|
1193
|
-
} else if (typeof val === "object" && val != null) {
|
|
1194
|
-
Object.assign(result, this._buildSelectDef(val, fullKey));
|
|
1195
|
-
} else {
|
|
1196
|
-
// Plain value (string, number, boolean, etc.) — convert to Expr
|
|
1197
|
-
result[fullKey] = expr.toExpr(val);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
return result;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
private _buildJoinDefs(joins: QueryableMetaJoin[]): SelectQueryDefJoin[] {
|
|
1205
|
-
const result: SelectQueryDefJoin[] = [];
|
|
1206
|
-
|
|
1207
|
-
for (const join of joins) {
|
|
1208
|
-
const joinQr = join.queryable;
|
|
1209
|
-
const selectDef = joinQr.getSelectQueryDef();
|
|
1210
|
-
|
|
1211
|
-
const joinDef: SelectQueryDefJoin = {
|
|
1212
|
-
...selectDef,
|
|
1213
|
-
as: joinQr.meta.as,
|
|
1214
|
-
isSingle: join.isSingle,
|
|
1215
|
-
};
|
|
1216
|
-
|
|
1217
|
-
result.push(joinDef);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
return result;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
getResultMeta(outputColumns?: string[]): ResultMeta {
|
|
1224
|
-
const columns: Record<string, ColumnPrimitiveStr> = {};
|
|
1225
|
-
const joins: Record<string, { isSingle: boolean }> = {};
|
|
1226
|
-
|
|
1227
|
-
const buildResultMeta = (cols: QueryableRecord<any>, prefix: string) => {
|
|
1228
|
-
for (const [key, val] of Object.entries(cols)) {
|
|
1229
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1230
|
-
if (outputColumns && !outputColumns.includes(fullKey)) continue;
|
|
1231
|
-
|
|
1232
|
-
if (val instanceof ExprUnit) {
|
|
1233
|
-
// primitive
|
|
1234
|
-
columns[fullKey] = val.dataType;
|
|
1235
|
-
} else if (Array.isArray(val)) {
|
|
1236
|
-
//
|
|
1237
|
-
if (val.length > 0) {
|
|
1238
|
-
joins[fullKey] = { isSingle: false };
|
|
1239
|
-
buildResultMeta(val[0], fullKey);
|
|
1240
|
-
}
|
|
1241
|
-
} else if (typeof val === "object") {
|
|
1242
|
-
// 단일
|
|
1243
|
-
joins[fullKey] = { isSingle: true };
|
|
1244
|
-
buildResultMeta(val, fullKey);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
buildResultMeta(this.meta.columns, "");
|
|
1250
|
-
|
|
1251
|
-
return { columns, joins };
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
//#endregion
|
|
1255
|
-
|
|
1256
|
-
//#region ========== [
|
|
1257
|
-
|
|
1258
|
-
/**
|
|
1259
|
-
* INSERT
|
|
1260
|
-
*
|
|
1261
|
-
* MSSQL의 1000개 제한을 위해
|
|
1262
|
-
*
|
|
1263
|
-
* @param records -
|
|
1264
|
-
* @param outputColumns -
|
|
1265
|
-
* @returns outputColumns 지정 시 삽입된 레코드
|
|
1266
|
-
*
|
|
1267
|
-
* @example
|
|
1268
|
-
* ```typescript
|
|
1269
|
-
* // 단순 삽입
|
|
1270
|
-
* await db.user().insert([
|
|
1271
|
-
* { name: "
|
|
1272
|
-
* ]);
|
|
1273
|
-
*
|
|
1274
|
-
* // 삽입 후 ID
|
|
1275
|
-
* const [inserted] = await db.user().insert(
|
|
1276
|
-
* [{ name: "
|
|
1277
|
-
* ["id"],
|
|
1278
|
-
* );
|
|
1279
|
-
* ```
|
|
1280
|
-
*/
|
|
1281
|
-
async insert(records: TFrom["$inferInsert"][]): Promise<void>;
|
|
1282
|
-
async insert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1283
|
-
records: TFrom["$inferInsert"][],
|
|
1284
|
-
outputColumns: K[],
|
|
1285
|
-
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
1286
|
-
async insert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1287
|
-
records: TFrom["$inferInsert"][],
|
|
1288
|
-
outputColumns?: K[],
|
|
1289
|
-
): Promise<Pick<TFrom["$inferColumns"], K>[] | void> {
|
|
1290
|
-
if (records.length === 0) {
|
|
1291
|
-
return outputColumns ? [] : undefined;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// MSSQL 1000개 제한을 위해 청크
|
|
1295
|
-
const CHUNK_SIZE = 1000;
|
|
1296
|
-
const allResults: Pick<TFrom["$inferColumns"], K>[] = [];
|
|
1297
|
-
|
|
1298
|
-
for (let i = 0; i < records.length; i += CHUNK_SIZE) {
|
|
1299
|
-
const chunk = records.slice(i, i + CHUNK_SIZE);
|
|
1300
|
-
|
|
1301
|
-
const results = await this.meta.db.executeDefs<Pick<TFrom["$inferColumns"], K>>(
|
|
1302
|
-
[this.getInsertQueryDef(chunk, outputColumns)],
|
|
1303
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1304
|
-
);
|
|
1305
|
-
|
|
1306
|
-
if (outputColumns) {
|
|
1307
|
-
allResults.push(...results[0]);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
if (outputColumns) {
|
|
1312
|
-
return allResults;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
/**
|
|
1317
|
-
* WHERE
|
|
1318
|
-
*
|
|
1319
|
-
* @param record -
|
|
1320
|
-
* @param outputColumns -
|
|
1321
|
-
* @returns outputColumns 지정 시 삽입된 레코드
|
|
1322
|
-
*
|
|
1323
|
-
* @example
|
|
1324
|
-
* ```typescript
|
|
1325
|
-
* await db.user()
|
|
1326
|
-
* .where((u) => [expr.eq(u.email, "test@test.com")])
|
|
1327
|
-
* .insertIfNotExists({ name: "
|
|
1328
|
-
* ```
|
|
1329
|
-
*/
|
|
1330
|
-
async insertIfNotExists(record: TFrom["$inferInsert"]): Promise<void>;
|
|
1331
|
-
async insertIfNotExists<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1332
|
-
record: TFrom["$inferInsert"],
|
|
1333
|
-
outputColumns: K[],
|
|
1334
|
-
): Promise<Pick<TFrom["$inferColumns"], K>>;
|
|
1335
|
-
async insertIfNotExists<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1336
|
-
record: TFrom["$inferInsert"],
|
|
1337
|
-
outputColumns?: K[],
|
|
1338
|
-
): Promise<Pick<TFrom["$inferColumns"], K> | void> {
|
|
1339
|
-
const results = await this.meta.db.executeDefs<Pick<TFrom["$inferColumns"], K>>(
|
|
1340
|
-
[this.getInsertIfNotExistsQueryDef(record)],
|
|
1341
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1342
|
-
);
|
|
1343
|
-
|
|
1344
|
-
if (outputColumns) {
|
|
1345
|
-
return results[0][0];
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
/**
|
|
1350
|
-
* INSERT INTO ... SELECT (현재 SELECT 결과를 다른
|
|
1351
|
-
*
|
|
1352
|
-
* @param targetTable -
|
|
1353
|
-
* @param outputColumns -
|
|
1354
|
-
* @returns outputColumns 지정 시 삽입된 레코드
|
|
1355
|
-
*
|
|
1356
|
-
* @example
|
|
1357
|
-
* ```typescript
|
|
1358
|
-
* await db.user()
|
|
1359
|
-
* .select((u) => ({ name: u.name, createdAt: u.createdAt }))
|
|
1360
|
-
* .where((u) => [expr.eq(u.isArchived, false)])
|
|
1361
|
-
* .insertInto(ArchivedUser);
|
|
1362
|
-
* ```
|
|
1363
|
-
*/
|
|
1364
|
-
async insertInto<TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>>(
|
|
1365
|
-
targetTable: TTable,
|
|
1366
|
-
): Promise<void>;
|
|
1367
|
-
async insertInto<
|
|
1368
|
-
TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>,
|
|
1369
|
-
TOut extends keyof TTable["$inferColumns"] & string,
|
|
1370
|
-
>(targetTable: TTable, outputColumns: TOut[]): Promise<Pick<TData, TOut>[]>;
|
|
1371
|
-
async insertInto<
|
|
1372
|
-
TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>,
|
|
1373
|
-
TOut extends keyof TTable["$inferColumns"] & string,
|
|
1374
|
-
>(targetTable: TTable, outputColumns?: TOut[]): Promise<Pick<TData, TOut>[] | void> {
|
|
1375
|
-
const results = await this.meta.db.executeDefs<Pick<TData, TOut>>(
|
|
1376
|
-
[this.getInsertIntoQueryDef(targetTable)],
|
|
1377
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1378
|
-
);
|
|
1379
|
-
|
|
1380
|
-
if (outputColumns) {
|
|
1381
|
-
return results[0];
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
getInsertQueryDef(
|
|
1386
|
-
records: TFrom["$inferInsert"][],
|
|
1387
|
-
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1388
|
-
): InsertQueryDef {
|
|
1389
|
-
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1390
|
-
const outputDef = this._getCudOutputDef();
|
|
1391
|
-
|
|
1392
|
-
// AI
|
|
1393
|
-
const overrideIdentity =
|
|
1394
|
-
outputDef.aiColName != null &&
|
|
1395
|
-
records.some((r) => (r as Record<string, unknown>)[outputDef.aiColName!] !== undefined);
|
|
1396
|
-
|
|
1397
|
-
return objClearUndefined({
|
|
1398
|
-
type: "insert",
|
|
1399
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1400
|
-
records,
|
|
1401
|
-
overrideIdentity: overrideIdentity || undefined,
|
|
1402
|
-
output: outputColumns
|
|
1403
|
-
? {
|
|
1404
|
-
columns: outputColumns,
|
|
1405
|
-
pkColNames: outputDef.pkColNames,
|
|
1406
|
-
aiColName: outputDef.aiColName,
|
|
1407
|
-
}
|
|
1408
|
-
: undefined,
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
getInsertIfNotExistsQueryDef(
|
|
1413
|
-
record: TFrom["$inferInsert"],
|
|
1414
|
-
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1415
|
-
): InsertIfNotExistsQueryDef {
|
|
1416
|
-
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1417
|
-
const outputDef = this._getCudOutputDef();
|
|
1418
|
-
|
|
1419
|
-
const { select: _, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1420
|
-
|
|
1421
|
-
return objClearUndefined({
|
|
1422
|
-
type: "insertIfNotExists",
|
|
1423
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1424
|
-
record,
|
|
1425
|
-
existsSelectQuery,
|
|
1426
|
-
output: outputColumns
|
|
1427
|
-
? {
|
|
1428
|
-
columns: outputColumns,
|
|
1429
|
-
pkColNames: outputDef.pkColNames,
|
|
1430
|
-
aiColName: outputDef.aiColName,
|
|
1431
|
-
}
|
|
1432
|
-
: undefined,
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
getInsertIntoQueryDef<TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>>(
|
|
1437
|
-
targetTable: TTable,
|
|
1438
|
-
outputColumns?: (keyof TTable["$inferColumns"] & string)[],
|
|
1439
|
-
): InsertIntoQueryDef {
|
|
1440
|
-
const outputDef = this._getCudOutputDef();
|
|
1441
|
-
|
|
1442
|
-
return objClearUndefined({
|
|
1443
|
-
type: "insertInto",
|
|
1444
|
-
table: this.meta.db.getQueryDefObjectName(targetTable),
|
|
1445
|
-
recordsSelectQuery: this.getSelectQueryDef(),
|
|
1446
|
-
output: outputColumns
|
|
1447
|
-
? {
|
|
1448
|
-
columns: outputColumns,
|
|
1449
|
-
pkColNames: outputDef.pkColNames,
|
|
1450
|
-
aiColName: outputDef.aiColName,
|
|
1451
|
-
}
|
|
1452
|
-
: undefined,
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
//#endregion
|
|
1457
|
-
|
|
1458
|
-
//#region ========== [
|
|
1459
|
-
|
|
1460
|
-
/**
|
|
1461
|
-
* UPDATE
|
|
1462
|
-
*
|
|
1463
|
-
* @param recordFwd -
|
|
1464
|
-
* @param outputColumns -
|
|
1465
|
-
* @returns outputColumns 지정 시 업데이트된 레코드
|
|
1466
|
-
*
|
|
1467
|
-
* @example
|
|
1468
|
-
* ```typescript
|
|
1469
|
-
* // 단순 업데이트
|
|
1470
|
-
* await db.user()
|
|
1471
|
-
* .where((u) => [expr.eq(u.id, 1)])
|
|
1472
|
-
* .update((u) => ({
|
|
1473
|
-
* name: expr.val("string", "
|
|
1474
|
-
* updatedAt: expr.val("DateTime", DateTime.now()),
|
|
1475
|
-
* }));
|
|
1476
|
-
*
|
|
1477
|
-
* // 기존
|
|
1478
|
-
* await db.product()
|
|
1479
|
-
* .update((p) => ({
|
|
1480
|
-
* price: expr.mul(p.price, expr.val("number", 1.1)),
|
|
1481
|
-
* }));
|
|
1482
|
-
* ```
|
|
1483
|
-
*/
|
|
1484
|
-
async update(
|
|
1485
|
-
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1486
|
-
): Promise<void>;
|
|
1487
|
-
async update<K extends keyof TFrom["$columns"] & string>(
|
|
1488
|
-
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1489
|
-
outputColumns: K[],
|
|
1490
|
-
): Promise<Pick<TFrom["$columns"], K>[]>;
|
|
1491
|
-
async update<K extends keyof TFrom["$columns"] & string>(
|
|
1492
|
-
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1493
|
-
outputColumns?: K[],
|
|
1494
|
-
): Promise<Pick<TFrom["$columns"], K>[] | void> {
|
|
1495
|
-
const results = await this.meta.db.executeDefs<Pick<TFrom["$columns"], K>>(
|
|
1496
|
-
[this.getUpdateQueryDef(recordFwd, outputColumns)],
|
|
1497
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1498
|
-
);
|
|
1499
|
-
|
|
1500
|
-
if (outputColumns) {
|
|
1501
|
-
return results[0];
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
/**
|
|
1506
|
-
* DELETE
|
|
1507
|
-
*
|
|
1508
|
-
* @param outputColumns -
|
|
1509
|
-
* @returns outputColumns 지정 시 삭제된 레코드
|
|
1510
|
-
*
|
|
1511
|
-
* @example
|
|
1512
|
-
* ```typescript
|
|
1513
|
-
* // 단순
|
|
1514
|
-
* await db.user()
|
|
1515
|
-
* .where((u) => [expr.eq(u.id, 1)])
|
|
1516
|
-
* .delete();
|
|
1517
|
-
*
|
|
1518
|
-
* // 삭제된
|
|
1519
|
-
* const deleted = await db.user()
|
|
1520
|
-
* .where((u) => [expr.eq(u.isExpired, true)])
|
|
1521
|
-
* .delete(["id", "name"]);
|
|
1522
|
-
* ```
|
|
1523
|
-
*/
|
|
1524
|
-
async delete(): Promise<void>;
|
|
1525
|
-
async delete<K extends keyof TFrom["$columns"] & string>(
|
|
1526
|
-
outputColumns: K[],
|
|
1527
|
-
): Promise<Pick<TFrom["$columns"], K>[]>;
|
|
1528
|
-
async delete<K extends keyof TFrom["$columns"] & string>(
|
|
1529
|
-
outputColumns?: K[],
|
|
1530
|
-
): Promise<Pick<TFrom["$columns"], K>[] | void> {
|
|
1531
|
-
const results = await this.meta.db.executeDefs<Pick<TFrom["$columns"], K>>(
|
|
1532
|
-
[this.getDeleteQueryDef(outputColumns)],
|
|
1533
|
-
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1534
|
-
);
|
|
1535
|
-
|
|
1536
|
-
if (outputColumns) {
|
|
1537
|
-
return results[0];
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
getUpdateQueryDef(
|
|
1542
|
-
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1543
|
-
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1544
|
-
): UpdateQueryDef {
|
|
1545
|
-
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1546
|
-
const outputDef = this._getCudOutputDef();
|
|
1547
|
-
|
|
1548
|
-
return objClearUndefined({
|
|
1549
|
-
type: "update",
|
|
1550
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1551
|
-
as: this.meta.as,
|
|
1552
|
-
record: this._buildSelectDef(recordFwd(this.meta.columns), ""),
|
|
1553
|
-
top: this.meta.top,
|
|
1554
|
-
where: this.meta.where?.map((w) => w.expr),
|
|
1555
|
-
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1556
|
-
limit: this.meta.limit,
|
|
1557
|
-
output: outputColumns
|
|
1558
|
-
? {
|
|
1559
|
-
columns: outputColumns,
|
|
1560
|
-
pkColNames: outputDef.pkColNames,
|
|
1561
|
-
aiColName: outputDef.aiColName,
|
|
1562
|
-
}
|
|
1563
|
-
: undefined,
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
getDeleteQueryDef(outputColumns?: (keyof TFrom["$inferColumns"] & string)[]): DeleteQueryDef {
|
|
1568
|
-
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1569
|
-
const outputDef = this._getCudOutputDef();
|
|
1570
|
-
|
|
1571
|
-
return objClearUndefined({
|
|
1572
|
-
type: "delete",
|
|
1573
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1574
|
-
as: this.meta.as,
|
|
1575
|
-
top: this.meta.top,
|
|
1576
|
-
where: this.meta.where?.map((w) => w.expr),
|
|
1577
|
-
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1578
|
-
limit: this.meta.limit,
|
|
1579
|
-
output: outputColumns
|
|
1580
|
-
? {
|
|
1581
|
-
columns: outputColumns,
|
|
1582
|
-
pkColNames: outputDef.pkColNames,
|
|
1583
|
-
aiColName: outputDef.aiColName,
|
|
1584
|
-
}
|
|
1585
|
-
: undefined,
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
//#endregion
|
|
1590
|
-
|
|
1591
|
-
//#region ========== [
|
|
1592
|
-
|
|
1593
|
-
/**
|
|
1594
|
-
* UPSERT (UPDATE or INSERT)
|
|
1595
|
-
*
|
|
1596
|
-
* WHERE
|
|
1597
|
-
*
|
|
1598
|
-
* @param updateFwd -
|
|
1599
|
-
* @param insertFwd -
|
|
1600
|
-
* @param outputColumns -
|
|
1601
|
-
* @returns outputColumns 지정 시 영향받은 레코드
|
|
1602
|
-
*
|
|
1603
|
-
* @example
|
|
1604
|
-
* ```typescript
|
|
1605
|
-
* // UPDATE/INSERT 동일
|
|
1606
|
-
* await db.user()
|
|
1607
|
-
* .where((u) => [expr.eq(u.email, "test@test.com")])
|
|
1608
|
-
* .upsert(() => ({
|
|
1609
|
-
* name: expr.val("string", "
|
|
1610
|
-
* email: expr.val("string", "test@test.com"),
|
|
1611
|
-
* }));
|
|
1612
|
-
*
|
|
1613
|
-
* // UPDATE/INSERT 다른
|
|
1614
|
-
* await db.user()
|
|
1615
|
-
* .where((u) => [expr.eq(u.email, "test@test.com")])
|
|
1616
|
-
* .upsert(
|
|
1617
|
-
* () => ({ loginCount: expr.val("number", 1) }),
|
|
1618
|
-
* (update) => ({ ...update, email: expr.val("string", "test@test.com") }),
|
|
1619
|
-
* );
|
|
1620
|
-
* ```
|
|
1621
|
-
*/
|
|
1622
|
-
async upsert(
|
|
1623
|
-
updateFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1624
|
-
): Promise<void>;
|
|
1625
|
-
async upsert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1626
|
-
insertFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1627
|
-
outputColumns?: K[],
|
|
1628
|
-
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
1629
|
-
async upsert<U extends QueryableWriteRecord<TFrom["$inferUpdate"]>>(
|
|
1630
|
-
updateFwd: (cols: QueryableRecord<TData>) => U,
|
|
1631
|
-
insertFwd: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1632
|
-
): Promise<void>;
|
|
1633
|
-
async upsert<
|
|
1634
|
-
U extends QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1635
|
-
K extends keyof TFrom["$inferColumns"] & string,
|
|
1636
|
-
>(
|
|
1637
|
-
updateFwd: (cols: QueryableRecord<TData>) => U,
|
|
1638
|
-
insertFwd: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1639
|
-
outputColumns?: K[],
|
|
1640
|
-
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
1641
|
-
async upsert<
|
|
1642
|
-
U extends QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1643
|
-
K extends keyof TFrom["$inferColumns"] & string,
|
|
1644
|
-
>(
|
|
1645
|
-
updateFwdOrInsertFwd:
|
|
1646
|
-
| ((cols: QueryableRecord<TData>) => U)
|
|
1647
|
-
| ((cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferInsert"]>),
|
|
1648
|
-
insertFwdOrOutputColumns?:
|
|
1649
|
-
| ((updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>)
|
|
1650
|
-
| K[],
|
|
1651
|
-
outputColumns?: K[],
|
|
1652
|
-
): Promise<Pick<TFrom["$inferColumns"], K>[] | void> {
|
|
1653
|
-
const updateRecordFwd = updateFwdOrInsertFwd as (cols: QueryableRecord<TData>) => U;
|
|
1654
|
-
|
|
1655
|
-
const insertRecordFwd = (
|
|
1656
|
-
insertFwdOrOutputColumns instanceof Function ? insertFwdOrOutputColumns : updateFwdOrInsertFwd
|
|
1657
|
-
) as (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>;
|
|
1658
|
-
|
|
1659
|
-
const realOutputColumns =
|
|
1660
|
-
insertFwdOrOutputColumns instanceof Function ? outputColumns : insertFwdOrOutputColumns;
|
|
1661
|
-
|
|
1662
|
-
const results = await this.meta.db.executeDefs<Pick<TFrom["$inferColumns"], K>>(
|
|
1663
|
-
[this.getUpsertQueryDef(updateRecordFwd, insertRecordFwd, realOutputColumns)],
|
|
1664
|
-
[realOutputColumns ? this.getResultMeta(realOutputColumns) : undefined],
|
|
1665
|
-
);
|
|
1666
|
-
|
|
1667
|
-
if (realOutputColumns) {
|
|
1668
|
-
return results[0];
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
getUpsertQueryDef<U extends QueryableWriteRecord<TFrom["$inferUpdate"]>>(
|
|
1673
|
-
updateRecordFwd: (cols: QueryableRecord<TData>) => U,
|
|
1674
|
-
insertRecordFwd: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1675
|
-
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1676
|
-
): UpsertQueryDef {
|
|
1677
|
-
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1678
|
-
const outputDef = this._getCudOutputDef();
|
|
1679
|
-
|
|
1680
|
-
const { select: _sel, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1681
|
-
|
|
1682
|
-
// updateRecord
|
|
1683
|
-
const updateQrRecord = updateRecordFwd(this.meta.columns);
|
|
1684
|
-
const updateRecord: Record<string, Expr> = {};
|
|
1685
|
-
for (const [key, value] of Object.entries(updateQrRecord)) {
|
|
1686
|
-
updateRecord[key] = expr.toExpr(value);
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
// insertRecord
|
|
1690
|
-
const insertRecordRaw = insertRecordFwd(updateQrRecord);
|
|
1691
|
-
const insertRecord = Object.fromEntries(
|
|
1692
|
-
Object.entries(insertRecordRaw).map(([key, value]) => [key, expr.toExpr(value)]),
|
|
1693
|
-
);
|
|
1694
|
-
|
|
1695
|
-
return objClearUndefined({
|
|
1696
|
-
type: "upsert",
|
|
1697
|
-
table: this.meta.db.getQueryDefObjectName(from),
|
|
1698
|
-
existsSelectQuery,
|
|
1699
|
-
updateRecord,
|
|
1700
|
-
insertRecord,
|
|
1701
|
-
output: outputColumns
|
|
1702
|
-
? {
|
|
1703
|
-
columns: outputColumns,
|
|
1704
|
-
pkColNames: outputDef.pkColNames,
|
|
1705
|
-
aiColName: outputDef.aiColName,
|
|
1706
|
-
}
|
|
1707
|
-
: undefined,
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
//#endregion
|
|
1712
|
-
|
|
1713
|
-
//#region ========== DDL Helper ==========
|
|
1714
|
-
|
|
1715
|
-
/**
|
|
1716
|
-
* FK
|
|
1717
|
-
*/
|
|
1718
|
-
async switchFk(switch_: "on" | "off"): Promise<void> {
|
|
1719
|
-
const from = this.meta.from;
|
|
1720
|
-
if (!(from instanceof TableBuilder) && !(from instanceof ViewBuilder)) {
|
|
1721
|
-
throw new Error(
|
|
1722
|
-
"switchFk는 TableBuilder 또는 ViewBuilder 기반 queryable에서만 사용할 수 있습니다.",
|
|
1723
|
-
);
|
|
1724
|
-
}
|
|
1725
|
-
await this.meta.db.switchFk(this.meta.db.getQueryDefObjectName(from), switch_);
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
//#endregion
|
|
1729
|
-
|
|
1730
|
-
//#region ========== CUD 공통 ==========
|
|
1731
|
-
|
|
1732
|
-
private _getCudOutputDef(): {
|
|
1733
|
-
pkColNames: string[];
|
|
1734
|
-
aiColName?: string;
|
|
1735
|
-
} {
|
|
1736
|
-
const from = this.meta.from;
|
|
1737
|
-
|
|
1738
|
-
if (from instanceof TableBuilder) {
|
|
1739
|
-
if (from.meta.columns == null) {
|
|
1740
|
-
throw new Error(
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
let aiColName: string | undefined;
|
|
1744
|
-
for (const [key, col] of Object.entries(from.meta.columns as ColumnBuilderRecord)) {
|
|
1745
|
-
if (col.meta.autoIncrement) {
|
|
1746
|
-
aiColName = key;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
return {
|
|
1751
|
-
pkColNames: from.meta.primaryKey ?? [],
|
|
1752
|
-
aiColName,
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
throw new Error("CUD
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
//#endregion
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
//#region ========== Helper Functions ==========
|
|
1763
|
-
|
|
1764
|
-
/**
|
|
1765
|
-
* FK
|
|
1766
|
-
*
|
|
1767
|
-
* @param fkCols - FK
|
|
1768
|
-
* @param targetTable - 참조 대상
|
|
1769
|
-
* @returns 매칭된 PK
|
|
1770
|
-
* @throws FK/PK
|
|
1771
|
-
*/
|
|
1772
|
-
export function getMatchedPrimaryKeys(
|
|
1773
|
-
fkCols: string[],
|
|
1774
|
-
targetTable: TableBuilder<any, any>,
|
|
1775
|
-
): string[] {
|
|
1776
|
-
const pk = targetTable.meta.primaryKey;
|
|
1777
|
-
if (pk == null || fkCols.length !== pk.length) {
|
|
1778
|
-
throw new Error(
|
|
1779
|
-
`FK/PK
|
|
1780
|
-
);
|
|
1781
|
-
}
|
|
1782
|
-
return pk;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
/**
|
|
1786
|
-
* 중첩 columns
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
* 중첩 키(posts.userId)는 평면화된 키로 유지한다.
|
|
1790
|
-
*
|
|
1791
|
-
* 예: posts[0].userId
|
|
1792
|
-
* 새 alias "T2"로
|
|
1793
|
-
*
|
|
1794
|
-
* @param columns -
|
|
1795
|
-
* @param alias - 새
|
|
1796
|
-
* @param keyPrefix - 현재 중첩 경로 (
|
|
1797
|
-
* @returns
|
|
1798
|
-
*/
|
|
1799
|
-
function transformColumnsAlias<TRecord extends DataRecord>(
|
|
1800
|
-
columns: QueryableRecord<TRecord>,
|
|
1801
|
-
alias: string,
|
|
1802
|
-
keyPrefix: string = "",
|
|
1803
|
-
): QueryableRecord<TRecord> {
|
|
1804
|
-
const result: Record<string, unknown> = {};
|
|
1805
|
-
|
|
1806
|
-
for (const [key, value] of Object.entries(columns as Record<string, unknown>)) {
|
|
1807
|
-
const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
|
|
1808
|
-
|
|
1809
|
-
if (value instanceof ExprUnit) {
|
|
1810
|
-
result[key] = expr.col(value.dataType, alias, fullKey);
|
|
1811
|
-
} else if (Array.isArray(value)) {
|
|
1812
|
-
if (value.length > 0) {
|
|
1813
|
-
result[key] = [
|
|
1814
|
-
transformColumnsAlias(value[0] as QueryableRecord<DataRecord>, alias, fullKey),
|
|
1815
|
-
];
|
|
1816
|
-
}
|
|
1817
|
-
} else if (typeof value === "object" && value != null) {
|
|
1818
|
-
result[key] = transformColumnsAlias(value as QueryableRecord<DataRecord>, alias, fullKey);
|
|
1819
|
-
} else {
|
|
1820
|
-
result[key] = value;
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
return result as QueryableRecord<TRecord>;
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
//#endregion
|
|
1828
|
-
|
|
1829
|
-
//#region ========== Types ==========
|
|
1830
|
-
|
|
1831
|
-
interface QueryableMeta<TData extends DataRecord> {
|
|
1832
|
-
db: DbContextBase;
|
|
1833
|
-
from?:
|
|
1834
|
-
| TableBuilder<any, any>
|
|
1835
|
-
| ViewBuilder<any, any, any>
|
|
1836
|
-
| Queryable<any, any>
|
|
1837
|
-
| Queryable<TData, any>[]
|
|
1838
|
-
| string;
|
|
1839
|
-
as: string;
|
|
1840
|
-
columns: QueryableRecord<TData>;
|
|
1841
|
-
isCustomColumns?: boolean;
|
|
1842
|
-
distinct?: boolean;
|
|
1843
|
-
top?: number;
|
|
1844
|
-
lock?: boolean;
|
|
1845
|
-
where?: WhereExprUnit[];
|
|
1846
|
-
joins?: QueryableMetaJoin[];
|
|
1847
|
-
orderBy?: [ExprUnit<ColumnPrimitive>, ("ASC" | "DESC")?][];
|
|
1848
|
-
limit?: [number, number];
|
|
1849
|
-
groupBy?: ExprUnit<ColumnPrimitive>[];
|
|
1850
|
-
having?: WhereExprUnit[];
|
|
1851
|
-
with?: { name: string; base: Queryable<any, any>; recursive: Queryable<any, any> };
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
interface QueryableMetaJoin {
|
|
1855
|
-
queryable: Queryable<any, any>;
|
|
1856
|
-
isSingle: boolean;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
export type QueryableRecord<TData extends DataRecord> = {
|
|
1860
|
-
[K in keyof TData]: TData[K] extends ColumnPrimitive
|
|
1861
|
-
? ExprUnit<TData[K]>
|
|
1862
|
-
: TData[K] extends (infer U)[]
|
|
1863
|
-
? U extends DataRecord
|
|
1864
|
-
? QueryableRecord<U>[]
|
|
1865
|
-
: never
|
|
1866
|
-
: TData[K] extends (infer U)[] | undefined
|
|
1867
|
-
? U extends DataRecord
|
|
1868
|
-
? NullableQueryableRecord<U>[] | undefined
|
|
1869
|
-
: never
|
|
1870
|
-
: TData[K] extends DataRecord
|
|
1871
|
-
? QueryableRecord<TData[K]>
|
|
1872
|
-
: TData[K] extends DataRecord | undefined
|
|
1873
|
-
? NullableQueryableRecord<Exclude<TData[K], undefined>> | undefined
|
|
1874
|
-
: never;
|
|
1875
|
-
};
|
|
1876
|
-
|
|
1877
|
-
export type QueryableWriteRecord<TData> = {
|
|
1878
|
-
[K in keyof TData]: TData[K] extends ColumnPrimitive ? ExprInput<TData[K]> : never;
|
|
1879
|
-
};
|
|
1880
|
-
|
|
1881
|
-
export type NullableQueryableRecord<TData extends DataRecord> = {
|
|
1882
|
-
// Primitive — always | undefined (LEFT JOIN NULL propagation)
|
|
1883
|
-
[K in keyof TData]: TData[K] extends ColumnPrimitive
|
|
1884
|
-
? ExprUnit<TData[K] | undefined>
|
|
1885
|
-
: TData[K] extends (infer U)[]
|
|
1886
|
-
? U extends DataRecord
|
|
1887
|
-
? NullableQueryableRecord<U>[]
|
|
1888
|
-
: never
|
|
1889
|
-
: TData[K] extends (infer U)[] | undefined
|
|
1890
|
-
? U extends DataRecord
|
|
1891
|
-
? NullableQueryableRecord<U>[] | undefined
|
|
1892
|
-
: never
|
|
1893
|
-
: TData[K] extends DataRecord
|
|
1894
|
-
? NullableQueryableRecord<TData[K]>
|
|
1895
|
-
: TData[K] extends DataRecord | undefined
|
|
1896
|
-
? NullableQueryableRecord<Exclude<TData[K], undefined>> | undefined
|
|
1897
|
-
: never;
|
|
1898
|
-
};
|
|
1899
|
-
|
|
1900
|
-
/**
|
|
1901
|
-
* QueryableRecord에서 DataRecord로
|
|
1902
|
-
*
|
|
1903
|
-
* ExprUnit<T>를 T로, 중첩
|
|
1904
|
-
*/
|
|
1905
|
-
export type UnwrapQueryableRecord<R> = {
|
|
1906
|
-
[K in keyof R]: R[K] extends ExprUnit<infer T>
|
|
1907
|
-
? T
|
|
1908
|
-
: NonNullable<R[K]> extends (infer U)[]
|
|
1909
|
-
? U extends Record<string, any>
|
|
1910
|
-
? UnwrapQueryableRecord<U>[] | Extract<R[K], undefined>
|
|
1911
|
-
: never
|
|
1912
|
-
: NonNullable<R[K]> extends Record<string, any>
|
|
1913
|
-
? UnwrapQueryableRecord<NonNullable<R[K]>> | Extract<R[K], undefined>
|
|
1914
|
-
: never;
|
|
1915
|
-
};
|
|
1916
|
-
|
|
1917
|
-
//#region ========== PathProxy - include용
|
|
1918
|
-
|
|
1919
|
-
/**
|
|
1920
|
-
* include()에서
|
|
1921
|
-
* ColumnPrimitive가 아닌 필드(FK, FKT
|
|
1922
|
-
*
|
|
1923
|
-
* @example
|
|
1924
|
-
* ```typescript
|
|
1925
|
-
* // item.user.company
|
|
1926
|
-
* db.post.include(item => item.user.company)
|
|
1927
|
-
*
|
|
1928
|
-
* // item.title은 string(ColumnPrimitive)이므로 컴파일 에러
|
|
1929
|
-
* db.post.include(item => item.title) // ❌ 에러
|
|
1930
|
-
* ```
|
|
1931
|
-
*/
|
|
1932
|
-
/**
|
|
1933
|
-
* 배열이면 요소
|
|
1934
|
-
*/
|
|
1935
|
-
type UnwrapArray<TArray> = TArray extends (infer TElement)[] ? TElement : TArray;
|
|
1936
|
-
|
|
1937
|
-
const PATH_SYMBOL = Symbol("path");
|
|
1938
|
-
|
|
1939
|
-
/**
|
|
1940
|
-
* include()용
|
|
1941
|
-
*/
|
|
1942
|
-
export type PathProxy<TObject> = {
|
|
1943
|
-
[K in keyof TObject as TObject[K] extends ColumnPrimitive ? never : K]-?: PathProxy<
|
|
1944
|
-
UnwrapArray<TObject[K]>
|
|
1945
|
-
>;
|
|
1946
|
-
} & { readonly [PATH_SYMBOL]: string[] };
|
|
1947
|
-
|
|
1948
|
-
/**
|
|
1949
|
-
* PathProxy
|
|
1950
|
-
* Proxy를 사용하여 프로퍼티 접근을 가로채고 경로를 수집
|
|
1951
|
-
*/
|
|
1952
|
-
function createPathProxy<TObject>(path: string[] = []): PathProxy<TObject> {
|
|
1953
|
-
return new Proxy({} as PathProxy<TObject>, {
|
|
1954
|
-
get(_, prop: string | symbol) {
|
|
1955
|
-
if (prop === PATH_SYMBOL) return path;
|
|
1956
|
-
if (typeof prop === "symbol") return undefined;
|
|
1957
|
-
return createPathProxy<unknown>([...path, prop]);
|
|
1958
|
-
},
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
//#endregion
|
|
1963
|
-
|
|
1964
|
-
/**
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
* DbContext에서
|
|
1968
|
-
*
|
|
1969
|
-
* @param db - DbContext
|
|
1970
|
-
* @param tableOrView - TableBuilder 또는 ViewBuilder
|
|
1971
|
-
* @param as - alias 지정 (
|
|
1972
|
-
* @returns Queryable을 반환하는
|
|
1973
|
-
*
|
|
1974
|
-
* @example
|
|
1975
|
-
* ```typescript
|
|
1976
|
-
* class AppDbContext extends DbContext {
|
|
1977
|
-
* // 호출 시마다 새로운 alias 할당
|
|
1978
|
-
* user = queryable(this, User);
|
|
1979
|
-
*
|
|
1980
|
-
* // 사용 예시
|
|
1981
|
-
* async getActiveUsers() {
|
|
1982
|
-
* return this.user()
|
|
1983
|
-
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1984
|
-
* .result();
|
|
1985
|
-
* }
|
|
1986
|
-
* }
|
|
1987
|
-
* ```
|
|
1988
|
-
*/
|
|
1989
|
-
export function queryable<TBuilder extends TableBuilder<any, any> | ViewBuilder<any, any, any>>(
|
|
1990
|
-
db: DbContextBase,
|
|
1991
|
-
tableOrView: TBuilder,
|
|
1992
|
-
as?: string,
|
|
1993
|
-
): () => Queryable<TBuilder["$infer"], TBuilder extends TableBuilder<any, any> ? TBuilder : never> {
|
|
1994
|
-
return () => {
|
|
1995
|
-
// as가 명시되지 않으면 db.getNextAlias() 사용 (카운터 증가)
|
|
1996
|
-
// as가 명시되면 그대로 사용 (카운터 증가 안함)
|
|
1997
|
-
const finalAs = as ?? db.getNextAlias();
|
|
1998
|
-
|
|
1999
|
-
// TableBuilder + columns
|
|
2000
|
-
if (tableOrView instanceof TableBuilder && tableOrView.meta.columns != null) {
|
|
2001
|
-
const columnDefs = tableOrView.meta.columns as ColumnBuilderRecord;
|
|
2002
|
-
|
|
2003
|
-
return new Queryable({
|
|
2004
|
-
db,
|
|
2005
|
-
from: tableOrView,
|
|
2006
|
-
as: finalAs,
|
|
2007
|
-
columns: Object.fromEntries(
|
|
2008
|
-
Object.entries(columnDefs).map(([key, colDef]) => [
|
|
2009
|
-
key,
|
|
2010
|
-
expr.col(colDef.meta.type, finalAs, key),
|
|
2011
|
-
]),
|
|
2012
|
-
),
|
|
2013
|
-
}) as any;
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// ViewBuilder + viewFn
|
|
2017
|
-
if (tableOrView instanceof ViewBuilder && tableOrView.meta.viewFn != null) {
|
|
2018
|
-
const baseQr = tableOrView.meta.viewFn(db);
|
|
2019
|
-
|
|
2020
|
-
// TFrom을 ViewBuilder로 설정하여
|
|
2021
|
-
return new Queryable({
|
|
2022
|
-
db,
|
|
2023
|
-
from: tableOrView,
|
|
2024
|
-
as: finalAs,
|
|
2025
|
-
columns: transformColumnsAlias(baseQr.meta.columns, finalAs),
|
|
2026
|
-
}) as any;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
throw new Error(
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
//#endregion
|
|
1
|
+
import { TableBuilder } from "../schema/table-builder";
|
|
2
|
+
import { ViewBuilder } from "../schema/view-builder";
|
|
3
|
+
|
|
4
|
+
import type { DataRecord, ResultMeta } from "../types/db";
|
|
5
|
+
import type {
|
|
6
|
+
DeleteQueryDef,
|
|
7
|
+
InsertIfNotExistsQueryDef,
|
|
8
|
+
InsertIntoQueryDef,
|
|
9
|
+
InsertQueryDef,
|
|
10
|
+
QueryDefObjectName,
|
|
11
|
+
SelectQueryDef,
|
|
12
|
+
SelectQueryDefJoin,
|
|
13
|
+
UpdateQueryDef,
|
|
14
|
+
UpsertQueryDef,
|
|
15
|
+
} from "../types/query-def";
|
|
16
|
+
import type { DbContextBase } from "../types/db-context-def";
|
|
17
|
+
import {
|
|
18
|
+
type ColumnBuilderRecord,
|
|
19
|
+
type DataToColumnBuilderRecord,
|
|
20
|
+
} from "../schema/factory/column-builder";
|
|
21
|
+
import type { ColumnPrimitive, ColumnPrimitiveStr } from "../types/column";
|
|
22
|
+
import type { WhereExprUnit, ExprInput } from "../expr/expr-unit";
|
|
23
|
+
import { ExprUnit } from "../expr/expr-unit";
|
|
24
|
+
import type { Expr } from "../types/expr";
|
|
25
|
+
import { ArgumentError, objClearUndefined } from "@simplysm/core-common";
|
|
26
|
+
import {
|
|
27
|
+
ForeignKeyBuilder,
|
|
28
|
+
ForeignKeyTargetBuilder,
|
|
29
|
+
RelationKeyBuilder,
|
|
30
|
+
RelationKeyTargetBuilder,
|
|
31
|
+
} from "../schema/factory/relation-builder";
|
|
32
|
+
import { parseSearchQuery } from "./search-parser";
|
|
33
|
+
import { expr } from "../expr/expr";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* JOIN query builder
|
|
37
|
+
*
|
|
38
|
+
* Used internally by join/joinSingle methods to specify the table to join
|
|
39
|
+
*/
|
|
40
|
+
class JoinQueryable {
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly _db: DbContextBase,
|
|
43
|
+
private readonly _joinAlias: string,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Specify the table to join
|
|
48
|
+
*
|
|
49
|
+
* @param table - Table to join
|
|
50
|
+
* @returns Joined Queryable
|
|
51
|
+
*/
|
|
52
|
+
from<T extends TableBuilder<any, any>>(table: T): Queryable<T["$infer"], T> {
|
|
53
|
+
return queryable(this._db, table, this._joinAlias)();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Directly specify columns in join result
|
|
58
|
+
*
|
|
59
|
+
* @param columns - Custom column definition
|
|
60
|
+
* @returns Queryable with custom columns applied
|
|
61
|
+
*/
|
|
62
|
+
select<R extends DataRecord>(columns: QueryableRecord<R>): Queryable<R, never> {
|
|
63
|
+
return new Queryable({
|
|
64
|
+
db: this._db,
|
|
65
|
+
as: this._joinAlias,
|
|
66
|
+
columns,
|
|
67
|
+
isCustomColumns: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Combine multiple Queryables with UNION
|
|
73
|
+
*
|
|
74
|
+
* @param queries - Array of Queryables to UNION (minimum 2)
|
|
75
|
+
* @returns UNION-ed Queryable
|
|
76
|
+
* @throws If less than 2 queryables are passed
|
|
77
|
+
*/
|
|
78
|
+
union<TData extends DataRecord>(...queries: Queryable<TData, any>[]): Queryable<TData, never> {
|
|
79
|
+
if (queries.length < 2) {
|
|
80
|
+
throw new ArgumentError("union requires at least 2 queryables.", {
|
|
81
|
+
provided: queries.length,
|
|
82
|
+
minimum: 2,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const first = queries[0];
|
|
87
|
+
|
|
88
|
+
return new Queryable({
|
|
89
|
+
db: first.meta.db,
|
|
90
|
+
from: queries, // stored as Queryable[] array
|
|
91
|
+
as: this._joinAlias,
|
|
92
|
+
columns: transformColumnsAlias(first.meta.columns, this._joinAlias, ""),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Recursive CTE (Common Table Expression) builder
|
|
99
|
+
*
|
|
100
|
+
* used internally in recursive() method, Defines the body of a recursive query
|
|
101
|
+
*
|
|
102
|
+
* @template TBaseData - Base query data type
|
|
103
|
+
*/
|
|
104
|
+
class RecursiveQueryable<TBaseData extends DataRecord> {
|
|
105
|
+
constructor(
|
|
106
|
+
private readonly _baseQr: Queryable<TBaseData, any>,
|
|
107
|
+
private readonly _cteName: string,
|
|
108
|
+
) {}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* specify the target table for recursive query
|
|
112
|
+
*
|
|
113
|
+
* @param table - Target table to recurse
|
|
114
|
+
* @returns Queryable with self property added (for self-reference)
|
|
115
|
+
*/
|
|
116
|
+
from<T extends TableBuilder<any, any>>(
|
|
117
|
+
table: T,
|
|
118
|
+
): Queryable<T["$infer"] & { self?: TBaseData[] }, T> {
|
|
119
|
+
const selfAlias = `${this._cteName}.self`;
|
|
120
|
+
|
|
121
|
+
return queryable(this._baseQr.meta.db, table, this._cteName)().join(
|
|
122
|
+
"self",
|
|
123
|
+
() =>
|
|
124
|
+
new Queryable<TBaseData, never>({
|
|
125
|
+
db: this._baseQr.meta.db,
|
|
126
|
+
from: this._cteName,
|
|
127
|
+
as: selfAlias,
|
|
128
|
+
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
129
|
+
isCustomColumns: false,
|
|
130
|
+
}),
|
|
131
|
+
) as any;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Directly specify columns in recursive query
|
|
136
|
+
*
|
|
137
|
+
* @param columns - Custom column definition
|
|
138
|
+
* @returns Queryable with self property added
|
|
139
|
+
*/
|
|
140
|
+
select<R extends DataRecord>(
|
|
141
|
+
columns: QueryableRecord<R>,
|
|
142
|
+
): Queryable<R & { self?: TBaseData[] }, never> {
|
|
143
|
+
const selfAlias = `${this._cteName}.self`;
|
|
144
|
+
|
|
145
|
+
return new Queryable<R, never>({
|
|
146
|
+
db: this._baseQr.meta.db,
|
|
147
|
+
as: this._cteName,
|
|
148
|
+
columns,
|
|
149
|
+
isCustomColumns: true,
|
|
150
|
+
}).join(
|
|
151
|
+
"self",
|
|
152
|
+
() =>
|
|
153
|
+
new Queryable<TBaseData, never>({
|
|
154
|
+
db: this._baseQr.meta.db,
|
|
155
|
+
from: this._cteName,
|
|
156
|
+
as: selfAlias,
|
|
157
|
+
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
158
|
+
isCustomColumns: false,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Combine multiple Queryables with UNION (for recursive query)
|
|
165
|
+
*
|
|
166
|
+
* @param queries - Array of Queryables to UNION (minimum 2)
|
|
167
|
+
* @returns UNION Queryable with self property added
|
|
168
|
+
* @throws If less than 2 queryables are passed
|
|
169
|
+
*/
|
|
170
|
+
union<TData extends DataRecord>(
|
|
171
|
+
...queries: Queryable<TData, any>[]
|
|
172
|
+
): Queryable<TData & { self?: TBaseData[] }, never> {
|
|
173
|
+
if (queries.length < 2) {
|
|
174
|
+
throw new ArgumentError("union requires at least 2 queryables.", {
|
|
175
|
+
provided: queries.length,
|
|
176
|
+
minimum: 2,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const first = queries[0];
|
|
181
|
+
|
|
182
|
+
const selfAlias = `${this._cteName}.self`;
|
|
183
|
+
|
|
184
|
+
return new Queryable<any, never>({
|
|
185
|
+
db: first.meta.db,
|
|
186
|
+
from: queries, // stored as Queryable[] array
|
|
187
|
+
as: this._cteName,
|
|
188
|
+
columns: transformColumnsAlias(first.meta.columns, this._cteName, ""),
|
|
189
|
+
}).join(
|
|
190
|
+
"self",
|
|
191
|
+
() =>
|
|
192
|
+
new Queryable({
|
|
193
|
+
db: this._baseQr.meta.db,
|
|
194
|
+
from: this._cteName,
|
|
195
|
+
as: selfAlias,
|
|
196
|
+
columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
|
|
197
|
+
isCustomColumns: false,
|
|
198
|
+
}),
|
|
199
|
+
) as any;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Query builder class
|
|
205
|
+
*
|
|
206
|
+
* Construct SELECT, INSERT, UPDATE, DELETE queries on tables/views in a chaining manner
|
|
207
|
+
*
|
|
208
|
+
* @template TData - Data type of query result
|
|
209
|
+
* @template TFrom - Source table (needed for CUD operations)
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* // Basic query
|
|
214
|
+
* const users = await db.user()
|
|
215
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
216
|
+
* .orderBy((u) => u.name)
|
|
217
|
+
* .result();
|
|
218
|
+
*
|
|
219
|
+
* // JOIN query
|
|
220
|
+
* const posts = await db.post()
|
|
221
|
+
* .include((p) => p.user)
|
|
222
|
+
* .result();
|
|
223
|
+
*
|
|
224
|
+
* // INSERT
|
|
225
|
+
* await db.user().insert([{ name: "Gildong Hong", email: "test@test.com" }]);
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export class Queryable<
|
|
229
|
+
TData extends DataRecord,
|
|
230
|
+
TFrom extends TableBuilder<any, any> | never, // Only TableBuilder is supported for CUD operations
|
|
231
|
+
> {
|
|
232
|
+
constructor(readonly meta: QueryableMeta<TData>) {}
|
|
233
|
+
|
|
234
|
+
//#region ========== option - SELECT / DISTINCT / LOCK ==========
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Specify columns to SELECT.
|
|
238
|
+
*
|
|
239
|
+
* @param fn - Column mapping function. Receives original columns and returns new column structure
|
|
240
|
+
* @returns Queryable with new column structure applied
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* db.user().select((u) => ({
|
|
245
|
+
* userName: u.name,
|
|
246
|
+
* userEmail: u.email,
|
|
247
|
+
* }))
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
select<R extends Record<string, any>>(
|
|
251
|
+
fn: (columns: QueryableRecord<TData>) => R,
|
|
252
|
+
): Queryable<UnwrapQueryableRecord<R>, never> {
|
|
253
|
+
if (Array.isArray(this.meta.from)) {
|
|
254
|
+
const newFroms = this.meta.from.map((from) => from.select(fn));
|
|
255
|
+
return new Queryable({
|
|
256
|
+
...this.meta,
|
|
257
|
+
from: newFroms,
|
|
258
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
259
|
+
}) as any;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const newColumns = fn(this.meta.columns);
|
|
263
|
+
|
|
264
|
+
return new Queryable<any, never>({
|
|
265
|
+
...this.meta,
|
|
266
|
+
columns: newColumns,
|
|
267
|
+
isCustomColumns: true,
|
|
268
|
+
}) as any;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Apply DISTINCT option to remove duplicate rows
|
|
273
|
+
*
|
|
274
|
+
* @returns Queryable with DISTINCT applied
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* db.user()
|
|
279
|
+
* .select((u) => ({ name: u.name }))
|
|
280
|
+
* .distinct()
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
distinct(): Queryable<TData, never> {
|
|
284
|
+
if (Array.isArray(this.meta.from)) {
|
|
285
|
+
const newFroms = this.meta.from.map((from) => from.distinct());
|
|
286
|
+
return new Queryable({
|
|
287
|
+
...this.meta,
|
|
288
|
+
from: newFroms,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return new Queryable({
|
|
293
|
+
...this.meta,
|
|
294
|
+
distinct: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Apply row lock (FOR UPDATE)
|
|
300
|
+
*
|
|
301
|
+
* Acquire exclusive lock on selected rows within transaction
|
|
302
|
+
*
|
|
303
|
+
* @returns Queryable with lock applied
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* await db.connect(async () => {
|
|
308
|
+
* const user = await db.user()
|
|
309
|
+
* .where((u) => [expr.eq(u.id, 1)])
|
|
310
|
+
* .lock()
|
|
311
|
+
* .single();
|
|
312
|
+
* });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
lock(): Queryable<TData, TFrom> {
|
|
316
|
+
if (Array.isArray(this.meta.from)) {
|
|
317
|
+
const newFroms = this.meta.from.map((from) => from.lock());
|
|
318
|
+
return new Queryable({
|
|
319
|
+
...this.meta,
|
|
320
|
+
from: newFroms,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return new Queryable({
|
|
325
|
+
...this.meta,
|
|
326
|
+
lock: true,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
|
|
332
|
+
//#region ========== restrict - TOP / LIMIT ==========
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Select only top N rows (can be used without ORDER BY)
|
|
336
|
+
*
|
|
337
|
+
* @param count - number of rows to select
|
|
338
|
+
* @returns Queryable with TOP applied
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* // Latest 10 users
|
|
343
|
+
* db.user()
|
|
344
|
+
* .orderBy((u) => u.createdAt, "DESC")
|
|
345
|
+
* .top(10)
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
top(count: number): Queryable<TData, TFrom> {
|
|
349
|
+
if (Array.isArray(this.meta.from)) {
|
|
350
|
+
const newFroms = this.meta.from.map((from) => from.top(count));
|
|
351
|
+
return new Queryable({
|
|
352
|
+
...this.meta,
|
|
353
|
+
from: newFroms,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return new Queryable({
|
|
358
|
+
...this.meta,
|
|
359
|
+
top: count,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Set LIMIT/OFFSET for pagination.
|
|
365
|
+
* Must call orderBy() first.
|
|
366
|
+
*
|
|
367
|
+
* @param skip - number of rows to skip (OFFSET)
|
|
368
|
+
* @param take - number of rows to fetch (LIMIT)
|
|
369
|
+
* @returns Queryable with pagination applied
|
|
370
|
+
* @throws Error if no ORDER BY clause
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* db.user
|
|
375
|
+
* .orderBy((u) => u.createdAt)
|
|
376
|
+
* .limit(0, 20) // first 20
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
limit(skip: number, take: number): Queryable<TData, TFrom> {
|
|
380
|
+
if (Array.isArray(this.meta.from)) {
|
|
381
|
+
const newFroms = this.meta.from.map((from) => from.limit(skip, take));
|
|
382
|
+
return new Queryable({
|
|
383
|
+
...this.meta,
|
|
384
|
+
from: newFroms,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!this.meta.orderBy) {
|
|
389
|
+
throw new ArgumentError("limit() requires ORDER BY clause.", {
|
|
390
|
+
method: "limit",
|
|
391
|
+
required: "orderBy",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return new Queryable({
|
|
396
|
+
...this.meta,
|
|
397
|
+
limit: [skip, take],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
//#endregion
|
|
402
|
+
|
|
403
|
+
//#region ========== sorting - ORDER BY ==========
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Add sorting condition. Multiple calls apply in order.
|
|
407
|
+
*
|
|
408
|
+
* @param fn - function returning columns to sort by
|
|
409
|
+
* @param orderBy - Sort direction (ASC/DESC). Default: ASC
|
|
410
|
+
* @returns sorting 조건이 추가된 Queryable
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* db.user
|
|
415
|
+
* .orderBy((u) => u.name) // 이름 ASC
|
|
416
|
+
* .orderBy((u) => u.age, "DESC") // 나이 DESC
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
orderBy(
|
|
420
|
+
fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>,
|
|
421
|
+
orderBy?: "ASC" | "DESC",
|
|
422
|
+
): Queryable<TData, TFrom> {
|
|
423
|
+
if (Array.isArray(this.meta.from)) {
|
|
424
|
+
const newFroms = this.meta.from.map((from) => from.orderBy(fn, orderBy));
|
|
425
|
+
return new Queryable({
|
|
426
|
+
...this.meta,
|
|
427
|
+
from: newFroms,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const column = fn(this.meta.columns);
|
|
432
|
+
|
|
433
|
+
return new Queryable({
|
|
434
|
+
...this.meta,
|
|
435
|
+
orderBy: [...(this.meta.orderBy ?? []), [column, orderBy]],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
|
|
441
|
+
//#region ========== 검색 - WHERE ==========
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* WHERE condition을 추가합니다. 여러 번 호출하면 AND로 결합됩니다.
|
|
445
|
+
*
|
|
446
|
+
* @param predicate - Condition 배열을 반환하는 function
|
|
447
|
+
* @returns 조건이 추가된 Queryable
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* ```typescript
|
|
451
|
+
* db.user
|
|
452
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
453
|
+
* .where((u) => [expr.gte(u.age, 18)])
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
where(predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[]): Queryable<TData, TFrom> {
|
|
457
|
+
if (Array.isArray(this.meta.from)) {
|
|
458
|
+
const newFroms = this.meta.from.map((from) => from.where(predicate));
|
|
459
|
+
return new Queryable({
|
|
460
|
+
...this.meta,
|
|
461
|
+
from: newFroms,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const conditions = predicate(this.meta.columns);
|
|
466
|
+
|
|
467
|
+
return new Queryable({
|
|
468
|
+
...this.meta,
|
|
469
|
+
where: [...(this.meta.where ?? []), ...conditions],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 텍스트 검색을 수행
|
|
475
|
+
*
|
|
476
|
+
* 검색 문법은 {@link parseSearchQuery}를 참조
|
|
477
|
+
* - 공백으로 구분된 단어는 OR condition
|
|
478
|
+
* - `+`로 시작하는 단어는 required include (AND Condition)
|
|
479
|
+
* - `-`로 시작하는 단어는 exclude (NOT Condition)
|
|
480
|
+
*
|
|
481
|
+
* @param fn - 검색 대상 column을 반환하는 function
|
|
482
|
+
* @param searchText - 검색 텍스트
|
|
483
|
+
* @returns 검색 조건이 추가된 Queryable
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* db.user()
|
|
488
|
+
* .search((u) => [u.name, u.email], "Gildong Hong -탈퇴")
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
search(
|
|
492
|
+
fn: (columns: QueryableRecord<TData>) => ExprUnit<string | undefined>[],
|
|
493
|
+
searchText: string,
|
|
494
|
+
): Queryable<TData, TFrom> {
|
|
495
|
+
if (Array.isArray(this.meta.from)) {
|
|
496
|
+
const newFroms = this.meta.from.map((from) => from.search(fn, searchText));
|
|
497
|
+
return new Queryable({
|
|
498
|
+
...this.meta,
|
|
499
|
+
from: newFroms,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (searchText.trim() === "") {
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const columns = fn(this.meta.columns);
|
|
508
|
+
const parsed = parseSearchQuery(searchText);
|
|
509
|
+
|
|
510
|
+
const conditions: WhereExprUnit[] = [];
|
|
511
|
+
|
|
512
|
+
// OR condition: 각 column에서 pattern 하나라도 매치하면 OK
|
|
513
|
+
if (parsed.or.length === 1) {
|
|
514
|
+
const pattern = parsed.or[0];
|
|
515
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
516
|
+
conditions.push(expr.or(columnMatches));
|
|
517
|
+
} else if (parsed.or.length > 1) {
|
|
518
|
+
const orConditions = parsed.or.map((pattern) => {
|
|
519
|
+
const columnMatches = columns.map((col) =>
|
|
520
|
+
expr.like(expr.lower(col), pattern.toLowerCase()),
|
|
521
|
+
);
|
|
522
|
+
return expr.or(columnMatches);
|
|
523
|
+
});
|
|
524
|
+
conditions.push(expr.or(orConditions));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// MUST condition: 각 pattern이 어떤 column에서든 매치해야 함 (AND)
|
|
528
|
+
for (const pattern of parsed.must) {
|
|
529
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
530
|
+
conditions.push(expr.or(columnMatches));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// NOT condition: 모든 column에서 매치하지 않아야 함 (AND NOT)
|
|
534
|
+
for (const pattern of parsed.not) {
|
|
535
|
+
const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
|
|
536
|
+
conditions.push(expr.not(expr.or(columnMatches)));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (conditions.length === 0) {
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return this.where(() => [expr.and(conditions)]);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
|
|
548
|
+
//#region ========== 그룹 - GROUP BY / HAVING ==========
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* GROUP BY 절을 Add
|
|
552
|
+
*
|
|
553
|
+
* @param fn - Group화 기준 column을 반환하는 function
|
|
554
|
+
* @returns Queryable with GROUP BY applied
|
|
555
|
+
*
|
|
556
|
+
* @example
|
|
557
|
+
* ```typescript
|
|
558
|
+
* db.order()
|
|
559
|
+
* .select((o) => ({
|
|
560
|
+
* userId: o.userId,
|
|
561
|
+
* totalAmount: expr.sum(o.amount),
|
|
562
|
+
* }))
|
|
563
|
+
* .groupBy((o) => [o.userId])
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
groupBy(
|
|
567
|
+
fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>[],
|
|
568
|
+
): Queryable<TData, never> {
|
|
569
|
+
if (Array.isArray(this.meta.from)) {
|
|
570
|
+
const newFroms = this.meta.from.map((from) => from.groupBy(fn));
|
|
571
|
+
return new Queryable({
|
|
572
|
+
...this.meta,
|
|
573
|
+
from: newFroms,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const groupBy = fn(this.meta.columns);
|
|
578
|
+
|
|
579
|
+
return new Queryable({ ...this.meta, groupBy });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* HAVING 절을 Add (GROUP BY 후 filtering)
|
|
584
|
+
*
|
|
585
|
+
* @param predicate - Condition 배열을 반환하는 function
|
|
586
|
+
* @returns HAVING이 apply된 Queryable
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```typescript
|
|
590
|
+
* db.order()
|
|
591
|
+
* .select((o) => ({
|
|
592
|
+
* userId: o.userId,
|
|
593
|
+
* totalAmount: expr.sum(o.amount),
|
|
594
|
+
* }))
|
|
595
|
+
* .groupBy((o) => [o.userId])
|
|
596
|
+
* .having((o) => [expr.gte(o.totalAmount, 10000)])
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
having(predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[]): Queryable<TData, never> {
|
|
600
|
+
if (Array.isArray(this.meta.from)) {
|
|
601
|
+
const newFroms = this.meta.from.map((from) => from.having(predicate));
|
|
602
|
+
return new Queryable({
|
|
603
|
+
...this.meta,
|
|
604
|
+
from: newFroms,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const conditions = predicate(this.meta.columns);
|
|
609
|
+
|
|
610
|
+
return new Queryable({
|
|
611
|
+
...this.meta,
|
|
612
|
+
having: [...(this.meta.having ?? []), ...conditions],
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
//#endregion
|
|
617
|
+
|
|
618
|
+
//#region ========== join - JOIN / JOIN SINGLE ==========
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 1:N 관계의 LEFT OUTER JOIN을 수행 (배열로 result Add)
|
|
622
|
+
*
|
|
623
|
+
* @param as - Result에 추가할 property 이름
|
|
624
|
+
* @param fwd - Join 조건을 정의하는 콜백 function
|
|
625
|
+
* @returns join 결과가 배열로 추가된 Queryable
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* ```typescript
|
|
629
|
+
* db.user()
|
|
630
|
+
* .join("posts", (qr, u) =>
|
|
631
|
+
* qr.from(Post)
|
|
632
|
+
* .where((p) => [expr.eq(p.userId, u.id)])
|
|
633
|
+
* )
|
|
634
|
+
* // Result: { id, name, posts: [{ id, title }, ...] }
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
join<A extends string, R extends DataRecord>(
|
|
638
|
+
as: A,
|
|
639
|
+
fwd: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>,
|
|
640
|
+
): Queryable<TData & { [K in A]?: R[] }, TFrom> {
|
|
641
|
+
if (Array.isArray(this.meta.from)) {
|
|
642
|
+
const newFroms = this.meta.from.map((from) => from.join(as, fwd));
|
|
643
|
+
return new Queryable({
|
|
644
|
+
...this.meta,
|
|
645
|
+
from: newFroms,
|
|
646
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 1. join alias Generate
|
|
651
|
+
const joinAlias = `${this.meta.as}.${as}`;
|
|
652
|
+
|
|
653
|
+
// 2. target → Queryable Transform (alias 전달)
|
|
654
|
+
const joinQr = new JoinQueryable(this.meta.db, joinAlias);
|
|
655
|
+
|
|
656
|
+
// 3. fwd 실행 (where 등 condition 추가된 Queryable return)
|
|
657
|
+
const resultQr = fwd(joinQr, this.meta.columns);
|
|
658
|
+
|
|
659
|
+
// 4. 새 columns에 join result Add
|
|
660
|
+
const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
|
|
661
|
+
|
|
662
|
+
return new Queryable({
|
|
663
|
+
...this.meta,
|
|
664
|
+
columns: {
|
|
665
|
+
...this.meta.columns,
|
|
666
|
+
[as]: [joinColumns],
|
|
667
|
+
} as QueryableRecord<any>,
|
|
668
|
+
isCustomColumns: true,
|
|
669
|
+
joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: false }],
|
|
670
|
+
}) as any;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* N:1 또는 1:1 관계의 LEFT OUTER JOIN을 수행 (단일 객체로 result Add)
|
|
675
|
+
*
|
|
676
|
+
* @param as - Result에 추가할 property 이름
|
|
677
|
+
* @param fwd - Join 조건을 정의하는 콜백 function
|
|
678
|
+
* @returns join 결과가 단일 객체로 추가된 Queryable
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* ```typescript
|
|
682
|
+
* db.post()
|
|
683
|
+
* .joinSingle("user", (qr, p) =>
|
|
684
|
+
* qr.from(User)
|
|
685
|
+
* .where((u) => [expr.eq(u.id, p.userId)])
|
|
686
|
+
* )
|
|
687
|
+
* // Result: { id, title, user: { id, name } | undefined }
|
|
688
|
+
* ```
|
|
689
|
+
*/
|
|
690
|
+
joinSingle<A extends string, R extends DataRecord>(
|
|
691
|
+
as: A,
|
|
692
|
+
fwd: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>,
|
|
693
|
+
): Queryable<
|
|
694
|
+
{ [K in keyof TData as K extends A ? never : K]: TData[K] } & { [K in A]?: R },
|
|
695
|
+
TFrom
|
|
696
|
+
> {
|
|
697
|
+
if (Array.isArray(this.meta.from)) {
|
|
698
|
+
const newFroms = this.meta.from.map((from) => from.joinSingle(as, fwd));
|
|
699
|
+
return new Queryable({
|
|
700
|
+
...this.meta,
|
|
701
|
+
from: newFroms,
|
|
702
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// 1. join alias Generate
|
|
707
|
+
const joinAlias = `${this.meta.as}.${as}`;
|
|
708
|
+
|
|
709
|
+
// 2. target → Queryable Transform (alias 전달)
|
|
710
|
+
const joinQr = new JoinQueryable(this.meta.db, joinAlias);
|
|
711
|
+
|
|
712
|
+
// 3. fwd 실행 (where 등 condition 추가된 Queryable return)
|
|
713
|
+
const resultQr = fwd(joinQr, this.meta.columns);
|
|
714
|
+
|
|
715
|
+
// 4. 새 columns에 join result Add
|
|
716
|
+
const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
|
|
717
|
+
|
|
718
|
+
return new Queryable({
|
|
719
|
+
...this.meta,
|
|
720
|
+
columns: {
|
|
721
|
+
...this.meta.columns,
|
|
722
|
+
[as]: joinColumns,
|
|
723
|
+
} as QueryableRecord<any>,
|
|
724
|
+
isCustomColumns: true,
|
|
725
|
+
joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: true }],
|
|
726
|
+
}) as any;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
//#endregion
|
|
730
|
+
|
|
731
|
+
//#region ========== join - INCLUDE ==========
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* 관계된 Table을 automatic으로 JOIN합니다.
|
|
735
|
+
* TableBuilder에 정의된 FK/FKT 관계를 기반으로 동작합니다.
|
|
736
|
+
*
|
|
737
|
+
* @param fn - 포함할 관계를 선택하는 function (PathProxy를 통해 type 체크됨)
|
|
738
|
+
* @returns JOIN이 추가된 Queryable
|
|
739
|
+
* @throws 관계가 정의되지 않은 경우 에러
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* // 단일 relationship include
|
|
744
|
+
* db.post.include((p) => p.user)
|
|
745
|
+
*
|
|
746
|
+
* // 중첩 relationship include
|
|
747
|
+
* db.post.include((p) => p.user.company)
|
|
748
|
+
*
|
|
749
|
+
* // 다중 relationship include
|
|
750
|
+
* db.user
|
|
751
|
+
* .include((u) => u.company)
|
|
752
|
+
* .include((u) => u.posts)
|
|
753
|
+
* ```
|
|
754
|
+
*/
|
|
755
|
+
include(fn: (item: PathProxy<TData>) => PathProxy<any>): Queryable<TData, TFrom> {
|
|
756
|
+
if (Array.isArray(this.meta.from)) {
|
|
757
|
+
const newFroms = this.meta.from.map((from) => from.include(fn));
|
|
758
|
+
return new Queryable({
|
|
759
|
+
...this.meta,
|
|
760
|
+
from: newFroms,
|
|
761
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const proxy = createPathProxy<TData>();
|
|
766
|
+
const result = fn(proxy);
|
|
767
|
+
const relationChain = result[PATH_SYMBOL].join(".");
|
|
768
|
+
|
|
769
|
+
return this._include(relationChain);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private _include(relationChain: string): Queryable<TData, TFrom> {
|
|
773
|
+
const relationNames = relationChain.split(".");
|
|
774
|
+
|
|
775
|
+
let result: Queryable<any, any> = this;
|
|
776
|
+
let currentTable = this.meta.from;
|
|
777
|
+
const chainParts: string[] = [];
|
|
778
|
+
|
|
779
|
+
for (const relationName of relationNames) {
|
|
780
|
+
if (!(currentTable instanceof TableBuilder)) {
|
|
781
|
+
throw new Error("include() can only be used on TableBuilder-based queryables.");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const parentChain = chainParts.join(".");
|
|
785
|
+
chainParts.push(relationName);
|
|
786
|
+
|
|
787
|
+
// 이미 JOIN된 경우 중복 Add 방지
|
|
788
|
+
const targetAlias = `${result.meta.as}.${chainParts.join(".")}`;
|
|
789
|
+
const existingJoin = result.meta.joins?.find((j) => j.queryable.meta.as === targetAlias);
|
|
790
|
+
if (existingJoin) {
|
|
791
|
+
// 기존 JOIN의 Table로 currentTable 업데이트 후 continue
|
|
792
|
+
const existingFrom = existingJoin.queryable.meta.from;
|
|
793
|
+
if (existingFrom instanceof TableBuilder) {
|
|
794
|
+
currentTable = existingFrom;
|
|
795
|
+
}
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const relationDef = currentTable.meta.relations?.[relationName];
|
|
800
|
+
if (relationDef == null) {
|
|
801
|
+
throw new Error(`Relation '${relationName}' not found.`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (relationDef instanceof ForeignKeyBuilder || relationDef instanceof RelationKeyBuilder) {
|
|
805
|
+
// FK/RelationKey (N:1): Post.user → User
|
|
806
|
+
// condition: Post.userId = User.id
|
|
807
|
+
const targetTable = relationDef.meta.targetFn();
|
|
808
|
+
const fkColKeys = relationDef.meta.columns;
|
|
809
|
+
const targetPkColKeys = getMatchedPrimaryKeys(fkColKeys, targetTable);
|
|
810
|
+
|
|
811
|
+
result = result.joinSingle(chainParts.join("."), (joinQr, parentCols) => {
|
|
812
|
+
const qr = joinQr.from(targetTable);
|
|
813
|
+
|
|
814
|
+
// FKT join은 배열로 저장되므로 배열인 경우 첫 번째 요소 사용
|
|
815
|
+
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
816
|
+
const srcCols = (
|
|
817
|
+
Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw
|
|
818
|
+
) as QueryableRecord<any>;
|
|
819
|
+
const conditions: WhereExprUnit[] = [];
|
|
820
|
+
|
|
821
|
+
for (let i = 0; i < fkColKeys.length; i++) {
|
|
822
|
+
const fkCol = srcCols[fkColKeys[i]];
|
|
823
|
+
const pkCol = qr.meta.columns[targetPkColKeys[i]] as ExprUnit<ColumnPrimitive>;
|
|
824
|
+
|
|
825
|
+
conditions.push(expr.eq(pkCol, fkCol));
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return qr.where(() => conditions);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
currentTable = targetTable;
|
|
832
|
+
} else if (
|
|
833
|
+
relationDef instanceof ForeignKeyTargetBuilder ||
|
|
834
|
+
relationDef instanceof RelationKeyTargetBuilder
|
|
835
|
+
) {
|
|
836
|
+
// FKT/RelationKeyTarget (1:N 또는 1:1): User.posts → Post[]
|
|
837
|
+
// condition: Post.userId = User.id
|
|
838
|
+
const targetTable = relationDef.meta.targetTableFn();
|
|
839
|
+
const fkRelName = relationDef.meta.relationName;
|
|
840
|
+
const sourceFk = targetTable.meta.relations?.[fkRelName];
|
|
841
|
+
if (!(sourceFk instanceof ForeignKeyBuilder) && !(sourceFk instanceof RelationKeyBuilder)) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`'${relationName}'이 참조하는 '${fkRelName}'이(가) ` +
|
|
844
|
+
`${targetTable.meta.name} Table의 유효한 ForeignKey/RelationKey가 아닙니다.`,
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const sourceTable = targetTable;
|
|
848
|
+
const isSingle: boolean = relationDef.meta.isSingle ?? false;
|
|
849
|
+
|
|
850
|
+
const fkColKeys = sourceFk.meta.columns;
|
|
851
|
+
const pkColKeys = getMatchedPrimaryKeys(fkColKeys, currentTable);
|
|
852
|
+
|
|
853
|
+
const buildJoin = (joinQr: JoinQueryable, parentCols: QueryableRecord<DataRecord>) => {
|
|
854
|
+
const qr = joinQr.from(sourceTable);
|
|
855
|
+
|
|
856
|
+
// FKT join은 배열로 저장되므로 배열인 경우 첫 번째 요소 사용
|
|
857
|
+
const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
|
|
858
|
+
const srcCols = (
|
|
859
|
+
Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw
|
|
860
|
+
) as QueryableRecord<any>;
|
|
861
|
+
const conditions: WhereExprUnit[] = [];
|
|
862
|
+
|
|
863
|
+
for (let i = 0; i < fkColKeys.length; i++) {
|
|
864
|
+
const pkCol = srcCols[pkColKeys[i]] as ExprUnit<ColumnPrimitive>;
|
|
865
|
+
const fkCol = qr.meta.columns[fkColKeys[i]] as ExprUnit<ColumnPrimitive>;
|
|
866
|
+
|
|
867
|
+
conditions.push(expr.eq(fkCol, pkCol));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return qr.where(() => conditions);
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
result = isSingle
|
|
874
|
+
? result.joinSingle(chainParts.join("."), buildJoin)
|
|
875
|
+
: result.join(chainParts.join("."), buildJoin);
|
|
876
|
+
|
|
877
|
+
currentTable = sourceTable;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return result as Queryable<TData, TFrom>;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
//#endregion
|
|
885
|
+
|
|
886
|
+
//#region ========== Subquery - WRAP / UNION ==========
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* 현재 Queryable을 Subquery로 감싸기
|
|
890
|
+
*
|
|
891
|
+
* distinct() 또는 groupBy() 후 count() 사용 시 필요
|
|
892
|
+
*
|
|
893
|
+
* @returns Subquery로 감싸진 Queryable
|
|
894
|
+
*
|
|
895
|
+
* @example
|
|
896
|
+
* ```typescript
|
|
897
|
+
* // DISTINCT 후 카운트
|
|
898
|
+
* const count = await db.user()
|
|
899
|
+
* .select((u) => ({ name: u.name }))
|
|
900
|
+
* .distinct()
|
|
901
|
+
* .wrap()
|
|
902
|
+
* .count();
|
|
903
|
+
* ```
|
|
904
|
+
*/
|
|
905
|
+
wrap(): Queryable<TData, never> {
|
|
906
|
+
// 현재 Queryable을 Subquery로 감싸기
|
|
907
|
+
const wrapAlias = this.meta.db.getNextAlias();
|
|
908
|
+
return new Queryable({
|
|
909
|
+
db: this.meta.db,
|
|
910
|
+
from: this,
|
|
911
|
+
as: wrapAlias,
|
|
912
|
+
columns: transformColumnsAlias<TData>(this.meta.columns, wrapAlias, ""),
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Combine multiple Queryables with UNION (remove duplicates)
|
|
918
|
+
*
|
|
919
|
+
* @param queries - Array of Queryables to UNION (minimum 2)
|
|
920
|
+
* @returns UNION-ed Queryable
|
|
921
|
+
* @throws If less than 2 queryables are passed
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```typescript
|
|
925
|
+
* const combined = Queryable.union(
|
|
926
|
+
* db.user().where((u) => [expr.eq(u.type, "admin")]),
|
|
927
|
+
* db.user().where((u) => [expr.eq(u.type, "manager")]),
|
|
928
|
+
* );
|
|
929
|
+
* ```
|
|
930
|
+
*/
|
|
931
|
+
static union<TData extends DataRecord>(
|
|
932
|
+
...queries: Queryable<TData, any>[]
|
|
933
|
+
): Queryable<TData, never> {
|
|
934
|
+
if (queries.length < 2) {
|
|
935
|
+
throw new ArgumentError("union requires at least 2 queryables.", {
|
|
936
|
+
provided: queries.length,
|
|
937
|
+
minimum: 2,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const first = queries[0];
|
|
942
|
+
const unionAlias = first.meta.db.getNextAlias();
|
|
943
|
+
return new Queryable({
|
|
944
|
+
db: first.meta.db,
|
|
945
|
+
from: queries, // stored as Queryable[] array
|
|
946
|
+
as: unionAlias,
|
|
947
|
+
columns: transformColumnsAlias(first.meta.columns, unionAlias, ""),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
//#endregion
|
|
952
|
+
|
|
953
|
+
//#region ========== recursive - WITH RECURSIVE ==========
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* recursive CTE(Common Table Expression)를 Generate
|
|
957
|
+
*
|
|
958
|
+
* 계층 structure data(조직도, 카테고리 트리 등)를 조회할 때 사용
|
|
959
|
+
*
|
|
960
|
+
* @param fwd - recursive part을 정의하는 콜백 function
|
|
961
|
+
* @returns recursive CTE가 apply된 Queryable
|
|
962
|
+
*
|
|
963
|
+
* @example
|
|
964
|
+
* ```typescript
|
|
965
|
+
* // 조직도 계층 조회
|
|
966
|
+
* db.employee()
|
|
967
|
+
* .where((e) => [expr.null(e.managerId)]) // 루트 노드
|
|
968
|
+
* .recursive((cte) =>
|
|
969
|
+
* cte.from(Employee)
|
|
970
|
+
* .where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
971
|
+
* )
|
|
972
|
+
* ```
|
|
973
|
+
*/
|
|
974
|
+
recursive(
|
|
975
|
+
fwd: (qr: RecursiveQueryable<TData>) => Queryable<TData, any>,
|
|
976
|
+
): Queryable<TData, never> {
|
|
977
|
+
if (Array.isArray(this.meta.from)) {
|
|
978
|
+
const newFroms = this.meta.from.map((from) => from.recursive(fwd));
|
|
979
|
+
return new Queryable({
|
|
980
|
+
...this.meta,
|
|
981
|
+
from: newFroms,
|
|
982
|
+
columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
// 동적 CTE 이름 Generate
|
|
986
|
+
const cteName = this.meta.db.getNextAlias();
|
|
987
|
+
|
|
988
|
+
// 2. target → Queryable Transform (CTE 이름 전달)
|
|
989
|
+
const cteQr = new RecursiveQueryable(this, cteName);
|
|
990
|
+
|
|
991
|
+
// 3. fwd 실행 (where 등 condition 추가된 Queryable return)
|
|
992
|
+
const resultQr = fwd(cteQr);
|
|
993
|
+
|
|
994
|
+
return new Queryable({
|
|
995
|
+
db: this.meta.db,
|
|
996
|
+
as: this.meta.as,
|
|
997
|
+
from: cteName,
|
|
998
|
+
columns: transformColumnsAlias(this.meta.columns, this.meta.as, ""),
|
|
999
|
+
with: {
|
|
1000
|
+
name: cteName,
|
|
1001
|
+
base: this as any, // circular 참조 Type inference 차단
|
|
1002
|
+
recursive: resultQr,
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
//#endregion
|
|
1008
|
+
|
|
1009
|
+
//#region ========== [query] 조회 - SELECT ==========
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* SELECT query를 실행하고 result 배열을 return
|
|
1013
|
+
*
|
|
1014
|
+
* @returns Query result array
|
|
1015
|
+
*
|
|
1016
|
+
* @example
|
|
1017
|
+
* ```typescript
|
|
1018
|
+
* const users = await db.user()
|
|
1019
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1020
|
+
* .result();
|
|
1021
|
+
* ```
|
|
1022
|
+
*/
|
|
1023
|
+
async result(): Promise<TData[]> {
|
|
1024
|
+
const results = await this.meta.db.executeDefs<TData>(
|
|
1025
|
+
[this.getSelectQueryDef()],
|
|
1026
|
+
[this.getResultMeta()],
|
|
1027
|
+
);
|
|
1028
|
+
return results[0];
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* 단일 결과를 return (2개 이상이면 Error)
|
|
1033
|
+
*
|
|
1034
|
+
* @returns 단일 result 또는 undefined
|
|
1035
|
+
* @throws 2개 이상의 결과가 반환된 경우
|
|
1036
|
+
*
|
|
1037
|
+
* @example
|
|
1038
|
+
* ```typescript
|
|
1039
|
+
* const user = await db.user()
|
|
1040
|
+
* .where((u) => [expr.eq(u.id, 1)])
|
|
1041
|
+
* .single();
|
|
1042
|
+
* ```
|
|
1043
|
+
*/
|
|
1044
|
+
async single(): Promise<TData | undefined> {
|
|
1045
|
+
const result = await this.top(2).result();
|
|
1046
|
+
if (result.length > 1) {
|
|
1047
|
+
throw new ArgumentError("Expected single result but multiple results returned.", {
|
|
1048
|
+
table: this._getSourceName(),
|
|
1049
|
+
resultCount: result.length,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return result[0];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* query 소스 이름 return (error message용)
|
|
1057
|
+
*/
|
|
1058
|
+
private _getSourceName(): string {
|
|
1059
|
+
const from = this.meta.from;
|
|
1060
|
+
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
1061
|
+
return from.meta.name;
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof from === "string") {
|
|
1064
|
+
return from;
|
|
1065
|
+
}
|
|
1066
|
+
return this.meta.as;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* 첫 번째 결과를 return (여러 개여도 첫 번째만)
|
|
1071
|
+
*
|
|
1072
|
+
* @returns 첫 번째 result 또는 undefined
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* ```typescript
|
|
1076
|
+
* const latestUser = await db.user()
|
|
1077
|
+
* .orderBy((u) => u.createdAt, "DESC")
|
|
1078
|
+
* .first();
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
async first(): Promise<TData | undefined> {
|
|
1082
|
+
const results = await this.top(1).result();
|
|
1083
|
+
return results[0];
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* result row 수를 return
|
|
1088
|
+
*
|
|
1089
|
+
* @param fwd - 카운트할 column을 지정하는 function (Select)
|
|
1090
|
+
* @returns row 수
|
|
1091
|
+
* @throws distinct() 또는 groupBy() 후 직접 호출 시 에러 (wrap() 필요)
|
|
1092
|
+
*
|
|
1093
|
+
* @example
|
|
1094
|
+
* ```typescript
|
|
1095
|
+
* const count = await db.user()
|
|
1096
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1097
|
+
* .count();
|
|
1098
|
+
* ```
|
|
1099
|
+
*/
|
|
1100
|
+
async count(fwd?: (cols: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>): Promise<number> {
|
|
1101
|
+
if (this.meta.distinct) {
|
|
1102
|
+
throw new Error("Cannot use count() after distinct(). Use wrap() first.");
|
|
1103
|
+
}
|
|
1104
|
+
if (this.meta.groupBy) {
|
|
1105
|
+
throw new Error("Cannot use count() after groupBy(). Use wrap() first.");
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const countQr = fwd
|
|
1109
|
+
? this.select((c) => ({ cnt: expr.count(fwd(c)) }))
|
|
1110
|
+
: this.select(() => ({ cnt: expr.count() }));
|
|
1111
|
+
|
|
1112
|
+
const result = await countQr.single();
|
|
1113
|
+
|
|
1114
|
+
return result?.cnt ?? 0;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* 조건에 맞는 data 존재 여부를 확인
|
|
1119
|
+
*
|
|
1120
|
+
* @returns 존재하면 true, 없으면 false
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* const hasAdmin = await db.user()
|
|
1125
|
+
* .where((u) => [expr.eq(u.role, "admin")])
|
|
1126
|
+
* .exists();
|
|
1127
|
+
* ```
|
|
1128
|
+
*/
|
|
1129
|
+
async exists(): Promise<boolean> {
|
|
1130
|
+
const count = await this.count();
|
|
1131
|
+
return count > 0;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
getSelectQueryDef(): SelectQueryDef {
|
|
1135
|
+
return objClearUndefined({
|
|
1136
|
+
type: "select",
|
|
1137
|
+
from: this._buildFromDef(),
|
|
1138
|
+
as: this.meta.as,
|
|
1139
|
+
select: this.meta.isCustomColumns ? this._buildSelectDef(this.meta.columns, "") : undefined,
|
|
1140
|
+
distinct: this.meta.distinct,
|
|
1141
|
+
top: this.meta.top,
|
|
1142
|
+
lock: this.meta.lock,
|
|
1143
|
+
where: this.meta.where?.map((w) => w.expr),
|
|
1144
|
+
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1145
|
+
orderBy: this.meta.orderBy?.map((o) => (o[1] ? [o[0].expr, o[1]] : [o[0].expr])),
|
|
1146
|
+
limit: this.meta.limit,
|
|
1147
|
+
groupBy: this.meta.groupBy?.map((g) => g.expr),
|
|
1148
|
+
having: this.meta.having?.map((w) => w.expr),
|
|
1149
|
+
with: this.meta.with
|
|
1150
|
+
? {
|
|
1151
|
+
name: this.meta.with.name,
|
|
1152
|
+
base: this.meta.with.base.getSelectQueryDef(),
|
|
1153
|
+
recursive: this.meta.with.recursive.getSelectQueryDef(),
|
|
1154
|
+
}
|
|
1155
|
+
: undefined,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private _buildFromDef():
|
|
1160
|
+
| QueryDefObjectName
|
|
1161
|
+
| SelectQueryDef
|
|
1162
|
+
| SelectQueryDef[]
|
|
1163
|
+
| string
|
|
1164
|
+
| undefined {
|
|
1165
|
+
const from = this.meta.from;
|
|
1166
|
+
|
|
1167
|
+
if (from instanceof TableBuilder || from instanceof ViewBuilder) {
|
|
1168
|
+
return this.meta.db.getQueryDefObjectName(from);
|
|
1169
|
+
} else if (from instanceof Queryable) {
|
|
1170
|
+
return from.getSelectQueryDef();
|
|
1171
|
+
} else if (Array.isArray(from)) {
|
|
1172
|
+
return from.map((qr) => qr.getSelectQueryDef());
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return from;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private _buildSelectDef(
|
|
1179
|
+
columns: QueryableRecord<any> | QueryableWriteRecord<any>,
|
|
1180
|
+
prefix: string,
|
|
1181
|
+
): Record<string, Expr> {
|
|
1182
|
+
const result: Record<string, Expr> = {};
|
|
1183
|
+
|
|
1184
|
+
for (const [key, val] of Object.entries(columns)) {
|
|
1185
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1186
|
+
|
|
1187
|
+
if (val instanceof ExprUnit) {
|
|
1188
|
+
result[fullKey] = val.expr;
|
|
1189
|
+
} else if (Array.isArray(val)) {
|
|
1190
|
+
if (val.length > 0) {
|
|
1191
|
+
Object.assign(result, this._buildSelectDef(val[0], fullKey));
|
|
1192
|
+
}
|
|
1193
|
+
} else if (typeof val === "object" && val != null) {
|
|
1194
|
+
Object.assign(result, this._buildSelectDef(val, fullKey));
|
|
1195
|
+
} else {
|
|
1196
|
+
// Plain value (string, number, boolean, etc.) — convert to Expr
|
|
1197
|
+
result[fullKey] = expr.toExpr(val);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return result;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private _buildJoinDefs(joins: QueryableMetaJoin[]): SelectQueryDefJoin[] {
|
|
1205
|
+
const result: SelectQueryDefJoin[] = [];
|
|
1206
|
+
|
|
1207
|
+
for (const join of joins) {
|
|
1208
|
+
const joinQr = join.queryable;
|
|
1209
|
+
const selectDef = joinQr.getSelectQueryDef();
|
|
1210
|
+
|
|
1211
|
+
const joinDef: SelectQueryDefJoin = {
|
|
1212
|
+
...selectDef,
|
|
1213
|
+
as: joinQr.meta.as,
|
|
1214
|
+
isSingle: join.isSingle,
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
result.push(joinDef);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
getResultMeta(outputColumns?: string[]): ResultMeta {
|
|
1224
|
+
const columns: Record<string, ColumnPrimitiveStr> = {};
|
|
1225
|
+
const joins: Record<string, { isSingle: boolean }> = {};
|
|
1226
|
+
|
|
1227
|
+
const buildResultMeta = (cols: QueryableRecord<any>, prefix: string) => {
|
|
1228
|
+
for (const [key, val] of Object.entries(cols)) {
|
|
1229
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1230
|
+
if (outputColumns && !outputColumns.includes(fullKey)) continue;
|
|
1231
|
+
|
|
1232
|
+
if (val instanceof ExprUnit) {
|
|
1233
|
+
// primitive column
|
|
1234
|
+
columns[fullKey] = val.dataType;
|
|
1235
|
+
} else if (Array.isArray(val)) {
|
|
1236
|
+
// array (1:N relationship)
|
|
1237
|
+
if (val.length > 0) {
|
|
1238
|
+
joins[fullKey] = { isSingle: false };
|
|
1239
|
+
buildResultMeta(val[0], fullKey);
|
|
1240
|
+
}
|
|
1241
|
+
} else if (typeof val === "object") {
|
|
1242
|
+
// 단일 object (N:1, 1:1 relationship)
|
|
1243
|
+
joins[fullKey] = { isSingle: true };
|
|
1244
|
+
buildResultMeta(val, fullKey);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
buildResultMeta(this.meta.columns, "");
|
|
1250
|
+
|
|
1251
|
+
return { columns, joins };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
//#endregion
|
|
1255
|
+
|
|
1256
|
+
//#region ========== [query] 삽입 - INSERT ==========
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* INSERT query를 실행
|
|
1260
|
+
*
|
|
1261
|
+
* MSSQL의 1000개 제한을 위해 automatic으로 1000개씩 청크로 분할하여 실행
|
|
1262
|
+
*
|
|
1263
|
+
* @param records - Insert할 레코드 array
|
|
1264
|
+
* @param outputColumns - column name array to receive (Select)
|
|
1265
|
+
* @returns outputColumns 지정 시 삽입된 레코드 array return
|
|
1266
|
+
*
|
|
1267
|
+
* @example
|
|
1268
|
+
* ```typescript
|
|
1269
|
+
* // 단순 삽입
|
|
1270
|
+
* await db.user().insert([
|
|
1271
|
+
* { name: "Gildong Hong", email: "hong@test.com" },
|
|
1272
|
+
* ]);
|
|
1273
|
+
*
|
|
1274
|
+
* // 삽입 후 ID return
|
|
1275
|
+
* const [inserted] = await db.user().insert(
|
|
1276
|
+
* [{ name: "Gildong Hong" }],
|
|
1277
|
+
* ["id"],
|
|
1278
|
+
* );
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
async insert(records: TFrom["$inferInsert"][]): Promise<void>;
|
|
1282
|
+
async insert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1283
|
+
records: TFrom["$inferInsert"][],
|
|
1284
|
+
outputColumns: K[],
|
|
1285
|
+
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
1286
|
+
async insert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1287
|
+
records: TFrom["$inferInsert"][],
|
|
1288
|
+
outputColumns?: K[],
|
|
1289
|
+
): Promise<Pick<TFrom["$inferColumns"], K>[] | void> {
|
|
1290
|
+
if (records.length === 0) {
|
|
1291
|
+
return outputColumns ? [] : undefined;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// MSSQL 1000개 제한을 위해 청크 split
|
|
1295
|
+
const CHUNK_SIZE = 1000;
|
|
1296
|
+
const allResults: Pick<TFrom["$inferColumns"], K>[] = [];
|
|
1297
|
+
|
|
1298
|
+
for (let i = 0; i < records.length; i += CHUNK_SIZE) {
|
|
1299
|
+
const chunk = records.slice(i, i + CHUNK_SIZE);
|
|
1300
|
+
|
|
1301
|
+
const results = await this.meta.db.executeDefs<Pick<TFrom["$inferColumns"], K>>(
|
|
1302
|
+
[this.getInsertQueryDef(chunk, outputColumns)],
|
|
1303
|
+
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
if (outputColumns) {
|
|
1307
|
+
allResults.push(...results[0]);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (outputColumns) {
|
|
1312
|
+
return allResults;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* WHERE condition에 맞는 data가 없으면 INSERT
|
|
1318
|
+
*
|
|
1319
|
+
* @param record - Insert할 레코드
|
|
1320
|
+
* @param outputColumns - column name array to receive (Select)
|
|
1321
|
+
* @returns outputColumns 지정 시 삽입된 레코드 return
|
|
1322
|
+
*
|
|
1323
|
+
* @example
|
|
1324
|
+
* ```typescript
|
|
1325
|
+
* await db.user()
|
|
1326
|
+
* .where((u) => [expr.eq(u.email, "test@test.com")])
|
|
1327
|
+
* .insertIfNotExists({ name: "testing", email: "test@test.com" });
|
|
1328
|
+
* ```
|
|
1329
|
+
*/
|
|
1330
|
+
async insertIfNotExists(record: TFrom["$inferInsert"]): Promise<void>;
|
|
1331
|
+
async insertIfNotExists<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1332
|
+
record: TFrom["$inferInsert"],
|
|
1333
|
+
outputColumns: K[],
|
|
1334
|
+
): Promise<Pick<TFrom["$inferColumns"], K>>;
|
|
1335
|
+
async insertIfNotExists<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1336
|
+
record: TFrom["$inferInsert"],
|
|
1337
|
+
outputColumns?: K[],
|
|
1338
|
+
): Promise<Pick<TFrom["$inferColumns"], K> | void> {
|
|
1339
|
+
const results = await this.meta.db.executeDefs<Pick<TFrom["$inferColumns"], K>>(
|
|
1340
|
+
[this.getInsertIfNotExistsQueryDef(record)],
|
|
1341
|
+
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
if (outputColumns) {
|
|
1345
|
+
return results[0][0];
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* INSERT INTO ... SELECT (현재 SELECT 결과를 다른 Table에 INSERT)
|
|
1351
|
+
*
|
|
1352
|
+
* @param targetTable - Insert 대상 Table
|
|
1353
|
+
* @param outputColumns - column name array to receive (Select)
|
|
1354
|
+
* @returns outputColumns 지정 시 삽입된 레코드 array return
|
|
1355
|
+
*
|
|
1356
|
+
* @example
|
|
1357
|
+
* ```typescript
|
|
1358
|
+
* await db.user()
|
|
1359
|
+
* .select((u) => ({ name: u.name, createdAt: u.createdAt }))
|
|
1360
|
+
* .where((u) => [expr.eq(u.isArchived, false)])
|
|
1361
|
+
* .insertInto(ArchivedUser);
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
async insertInto<TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>>(
|
|
1365
|
+
targetTable: TTable,
|
|
1366
|
+
): Promise<void>;
|
|
1367
|
+
async insertInto<
|
|
1368
|
+
TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>,
|
|
1369
|
+
TOut extends keyof TTable["$inferColumns"] & string,
|
|
1370
|
+
>(targetTable: TTable, outputColumns: TOut[]): Promise<Pick<TData, TOut>[]>;
|
|
1371
|
+
async insertInto<
|
|
1372
|
+
TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>,
|
|
1373
|
+
TOut extends keyof TTable["$inferColumns"] & string,
|
|
1374
|
+
>(targetTable: TTable, outputColumns?: TOut[]): Promise<Pick<TData, TOut>[] | void> {
|
|
1375
|
+
const results = await this.meta.db.executeDefs<Pick<TData, TOut>>(
|
|
1376
|
+
[this.getInsertIntoQueryDef(targetTable)],
|
|
1377
|
+
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
if (outputColumns) {
|
|
1381
|
+
return results[0];
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
getInsertQueryDef(
|
|
1386
|
+
records: TFrom["$inferInsert"][],
|
|
1387
|
+
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1388
|
+
): InsertQueryDef {
|
|
1389
|
+
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1390
|
+
const outputDef = this._getCudOutputDef();
|
|
1391
|
+
|
|
1392
|
+
// AI column에 explicit 값이 있으면 overrideIdentity 설정
|
|
1393
|
+
const overrideIdentity =
|
|
1394
|
+
outputDef.aiColName != null &&
|
|
1395
|
+
records.some((r) => (r as Record<string, unknown>)[outputDef.aiColName!] !== undefined);
|
|
1396
|
+
|
|
1397
|
+
return objClearUndefined({
|
|
1398
|
+
type: "insert",
|
|
1399
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1400
|
+
records,
|
|
1401
|
+
overrideIdentity: overrideIdentity || undefined,
|
|
1402
|
+
output: outputColumns
|
|
1403
|
+
? {
|
|
1404
|
+
columns: outputColumns,
|
|
1405
|
+
pkColNames: outputDef.pkColNames,
|
|
1406
|
+
aiColName: outputDef.aiColName,
|
|
1407
|
+
}
|
|
1408
|
+
: undefined,
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
getInsertIfNotExistsQueryDef(
|
|
1413
|
+
record: TFrom["$inferInsert"],
|
|
1414
|
+
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1415
|
+
): InsertIfNotExistsQueryDef {
|
|
1416
|
+
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1417
|
+
const outputDef = this._getCudOutputDef();
|
|
1418
|
+
|
|
1419
|
+
const { select: _, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1420
|
+
|
|
1421
|
+
return objClearUndefined({
|
|
1422
|
+
type: "insertIfNotExists",
|
|
1423
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1424
|
+
record,
|
|
1425
|
+
existsSelectQuery,
|
|
1426
|
+
output: outputColumns
|
|
1427
|
+
? {
|
|
1428
|
+
columns: outputColumns,
|
|
1429
|
+
pkColNames: outputDef.pkColNames,
|
|
1430
|
+
aiColName: outputDef.aiColName,
|
|
1431
|
+
}
|
|
1432
|
+
: undefined,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
getInsertIntoQueryDef<TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>>(
|
|
1437
|
+
targetTable: TTable,
|
|
1438
|
+
outputColumns?: (keyof TTable["$inferColumns"] & string)[],
|
|
1439
|
+
): InsertIntoQueryDef {
|
|
1440
|
+
const outputDef = this._getCudOutputDef();
|
|
1441
|
+
|
|
1442
|
+
return objClearUndefined({
|
|
1443
|
+
type: "insertInto",
|
|
1444
|
+
table: this.meta.db.getQueryDefObjectName(targetTable),
|
|
1445
|
+
recordsSelectQuery: this.getSelectQueryDef(),
|
|
1446
|
+
output: outputColumns
|
|
1447
|
+
? {
|
|
1448
|
+
columns: outputColumns,
|
|
1449
|
+
pkColNames: outputDef.pkColNames,
|
|
1450
|
+
aiColName: outputDef.aiColName,
|
|
1451
|
+
}
|
|
1452
|
+
: undefined,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
//#endregion
|
|
1457
|
+
|
|
1458
|
+
//#region ========== [query] Modify - UPDATE / DELETE ==========
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* UPDATE query를 실행
|
|
1462
|
+
*
|
|
1463
|
+
* @param recordFwd - Update할 column과 값을 반환하는 function
|
|
1464
|
+
* @param outputColumns - column name array to receive (Select)
|
|
1465
|
+
* @returns outputColumns 지정 시 업데이트된 레코드 array return
|
|
1466
|
+
*
|
|
1467
|
+
* @example
|
|
1468
|
+
* ```typescript
|
|
1469
|
+
* // 단순 업데이트
|
|
1470
|
+
* await db.user()
|
|
1471
|
+
* .where((u) => [expr.eq(u.id, 1)])
|
|
1472
|
+
* .update((u) => ({
|
|
1473
|
+
* name: expr.val("string", "New Name"),
|
|
1474
|
+
* updatedAt: expr.val("DateTime", DateTime.now()),
|
|
1475
|
+
* }));
|
|
1476
|
+
*
|
|
1477
|
+
* // 기존 value 참조
|
|
1478
|
+
* await db.product()
|
|
1479
|
+
* .update((p) => ({
|
|
1480
|
+
* price: expr.mul(p.price, expr.val("number", 1.1)),
|
|
1481
|
+
* }));
|
|
1482
|
+
* ```
|
|
1483
|
+
*/
|
|
1484
|
+
async update(
|
|
1485
|
+
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1486
|
+
): Promise<void>;
|
|
1487
|
+
async update<K extends keyof TFrom["$columns"] & string>(
|
|
1488
|
+
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1489
|
+
outputColumns: K[],
|
|
1490
|
+
): Promise<Pick<TFrom["$columns"], K>[]>;
|
|
1491
|
+
async update<K extends keyof TFrom["$columns"] & string>(
|
|
1492
|
+
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1493
|
+
outputColumns?: K[],
|
|
1494
|
+
): Promise<Pick<TFrom["$columns"], K>[] | void> {
|
|
1495
|
+
const results = await this.meta.db.executeDefs<Pick<TFrom["$columns"], K>>(
|
|
1496
|
+
[this.getUpdateQueryDef(recordFwd, outputColumns)],
|
|
1497
|
+
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
if (outputColumns) {
|
|
1501
|
+
return results[0];
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* DELETE query를 실행
|
|
1507
|
+
*
|
|
1508
|
+
* @param outputColumns - column name array to receive (Select)
|
|
1509
|
+
* @returns outputColumns 지정 시 삭제된 레코드 array return
|
|
1510
|
+
*
|
|
1511
|
+
* @example
|
|
1512
|
+
* ```typescript
|
|
1513
|
+
* // 단순 Delete
|
|
1514
|
+
* await db.user()
|
|
1515
|
+
* .where((u) => [expr.eq(u.id, 1)])
|
|
1516
|
+
* .delete();
|
|
1517
|
+
*
|
|
1518
|
+
* // 삭제된 data return
|
|
1519
|
+
* const deleted = await db.user()
|
|
1520
|
+
* .where((u) => [expr.eq(u.isExpired, true)])
|
|
1521
|
+
* .delete(["id", "name"]);
|
|
1522
|
+
* ```
|
|
1523
|
+
*/
|
|
1524
|
+
async delete(): Promise<void>;
|
|
1525
|
+
async delete<K extends keyof TFrom["$columns"] & string>(
|
|
1526
|
+
outputColumns: K[],
|
|
1527
|
+
): Promise<Pick<TFrom["$columns"], K>[]>;
|
|
1528
|
+
async delete<K extends keyof TFrom["$columns"] & string>(
|
|
1529
|
+
outputColumns?: K[],
|
|
1530
|
+
): Promise<Pick<TFrom["$columns"], K>[] | void> {
|
|
1531
|
+
const results = await this.meta.db.executeDefs<Pick<TFrom["$columns"], K>>(
|
|
1532
|
+
[this.getDeleteQueryDef(outputColumns)],
|
|
1533
|
+
outputColumns ? [this.getResultMeta(outputColumns)] : undefined,
|
|
1534
|
+
);
|
|
1535
|
+
|
|
1536
|
+
if (outputColumns) {
|
|
1537
|
+
return results[0];
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
getUpdateQueryDef(
|
|
1542
|
+
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1543
|
+
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1544
|
+
): UpdateQueryDef {
|
|
1545
|
+
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1546
|
+
const outputDef = this._getCudOutputDef();
|
|
1547
|
+
|
|
1548
|
+
return objClearUndefined({
|
|
1549
|
+
type: "update",
|
|
1550
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1551
|
+
as: this.meta.as,
|
|
1552
|
+
record: this._buildSelectDef(recordFwd(this.meta.columns), ""),
|
|
1553
|
+
top: this.meta.top,
|
|
1554
|
+
where: this.meta.where?.map((w) => w.expr),
|
|
1555
|
+
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1556
|
+
limit: this.meta.limit,
|
|
1557
|
+
output: outputColumns
|
|
1558
|
+
? {
|
|
1559
|
+
columns: outputColumns,
|
|
1560
|
+
pkColNames: outputDef.pkColNames,
|
|
1561
|
+
aiColName: outputDef.aiColName,
|
|
1562
|
+
}
|
|
1563
|
+
: undefined,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
getDeleteQueryDef(outputColumns?: (keyof TFrom["$inferColumns"] & string)[]): DeleteQueryDef {
|
|
1568
|
+
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1569
|
+
const outputDef = this._getCudOutputDef();
|
|
1570
|
+
|
|
1571
|
+
return objClearUndefined({
|
|
1572
|
+
type: "delete",
|
|
1573
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1574
|
+
as: this.meta.as,
|
|
1575
|
+
top: this.meta.top,
|
|
1576
|
+
where: this.meta.where?.map((w) => w.expr),
|
|
1577
|
+
joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
|
|
1578
|
+
limit: this.meta.limit,
|
|
1579
|
+
output: outputColumns
|
|
1580
|
+
? {
|
|
1581
|
+
columns: outputColumns,
|
|
1582
|
+
pkColNames: outputDef.pkColNames,
|
|
1583
|
+
aiColName: outputDef.aiColName,
|
|
1584
|
+
}
|
|
1585
|
+
: undefined,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
//#endregion
|
|
1590
|
+
|
|
1591
|
+
//#region ========== [query] Modify - UPSERT ==========
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* UPSERT (UPDATE or INSERT) query를 실행
|
|
1595
|
+
*
|
|
1596
|
+
* WHERE condition에 맞는 data가 있으면 UPDATE, 없으면 INSERT
|
|
1597
|
+
*
|
|
1598
|
+
* @param updateFwd - Update할 column과 값을 반환하는 function
|
|
1599
|
+
* @param insertFwd - Insert할 레코드를 반환하는 function (selection, 미지정 시 updateFwd와 동일)
|
|
1600
|
+
* @param outputColumns - column name array to receive (Select)
|
|
1601
|
+
* @returns outputColumns 지정 시 영향받은 레코드 array return
|
|
1602
|
+
*
|
|
1603
|
+
* @example
|
|
1604
|
+
* ```typescript
|
|
1605
|
+
* // UPDATE/INSERT 동일 data
|
|
1606
|
+
* await db.user()
|
|
1607
|
+
* .where((u) => [expr.eq(u.email, "test@test.com")])
|
|
1608
|
+
* .upsert(() => ({
|
|
1609
|
+
* name: expr.val("string", "testing"),
|
|
1610
|
+
* email: expr.val("string", "test@test.com"),
|
|
1611
|
+
* }));
|
|
1612
|
+
*
|
|
1613
|
+
* // UPDATE/INSERT 다른 data
|
|
1614
|
+
* await db.user()
|
|
1615
|
+
* .where((u) => [expr.eq(u.email, "test@test.com")])
|
|
1616
|
+
* .upsert(
|
|
1617
|
+
* () => ({ loginCount: expr.val("number", 1) }),
|
|
1618
|
+
* (update) => ({ ...update, email: expr.val("string", "test@test.com") }),
|
|
1619
|
+
* );
|
|
1620
|
+
* ```
|
|
1621
|
+
*/
|
|
1622
|
+
async upsert(
|
|
1623
|
+
updateFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1624
|
+
): Promise<void>;
|
|
1625
|
+
async upsert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
1626
|
+
insertFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1627
|
+
outputColumns?: K[],
|
|
1628
|
+
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
1629
|
+
async upsert<U extends QueryableWriteRecord<TFrom["$inferUpdate"]>>(
|
|
1630
|
+
updateFwd: (cols: QueryableRecord<TData>) => U,
|
|
1631
|
+
insertFwd: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1632
|
+
): Promise<void>;
|
|
1633
|
+
async upsert<
|
|
1634
|
+
U extends QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1635
|
+
K extends keyof TFrom["$inferColumns"] & string,
|
|
1636
|
+
>(
|
|
1637
|
+
updateFwd: (cols: QueryableRecord<TData>) => U,
|
|
1638
|
+
insertFwd: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1639
|
+
outputColumns?: K[],
|
|
1640
|
+
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
1641
|
+
async upsert<
|
|
1642
|
+
U extends QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
1643
|
+
K extends keyof TFrom["$inferColumns"] & string,
|
|
1644
|
+
>(
|
|
1645
|
+
updateFwdOrInsertFwd:
|
|
1646
|
+
| ((cols: QueryableRecord<TData>) => U)
|
|
1647
|
+
| ((cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferInsert"]>),
|
|
1648
|
+
insertFwdOrOutputColumns?:
|
|
1649
|
+
| ((updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>)
|
|
1650
|
+
| K[],
|
|
1651
|
+
outputColumns?: K[],
|
|
1652
|
+
): Promise<Pick<TFrom["$inferColumns"], K>[] | void> {
|
|
1653
|
+
const updateRecordFwd = updateFwdOrInsertFwd as (cols: QueryableRecord<TData>) => U;
|
|
1654
|
+
|
|
1655
|
+
const insertRecordFwd = (
|
|
1656
|
+
insertFwdOrOutputColumns instanceof Function ? insertFwdOrOutputColumns : updateFwdOrInsertFwd
|
|
1657
|
+
) as (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>;
|
|
1658
|
+
|
|
1659
|
+
const realOutputColumns =
|
|
1660
|
+
insertFwdOrOutputColumns instanceof Function ? outputColumns : insertFwdOrOutputColumns;
|
|
1661
|
+
|
|
1662
|
+
const results = await this.meta.db.executeDefs<Pick<TFrom["$inferColumns"], K>>(
|
|
1663
|
+
[this.getUpsertQueryDef(updateRecordFwd, insertRecordFwd, realOutputColumns)],
|
|
1664
|
+
[realOutputColumns ? this.getResultMeta(realOutputColumns) : undefined],
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
if (realOutputColumns) {
|
|
1668
|
+
return results[0];
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
getUpsertQueryDef<U extends QueryableWriteRecord<TFrom["$inferUpdate"]>>(
|
|
1673
|
+
updateRecordFwd: (cols: QueryableRecord<TData>) => U,
|
|
1674
|
+
insertRecordFwd: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
1675
|
+
outputColumns?: (keyof TFrom["$inferColumns"] & string)[],
|
|
1676
|
+
): UpsertQueryDef {
|
|
1677
|
+
const from = this.meta.from as TableBuilder<any, any> | ViewBuilder<any, any, any>;
|
|
1678
|
+
const outputDef = this._getCudOutputDef();
|
|
1679
|
+
|
|
1680
|
+
const { select: _sel, ...existsSelectQuery } = this.getSelectQueryDef();
|
|
1681
|
+
|
|
1682
|
+
// updateRecord Generate
|
|
1683
|
+
const updateQrRecord = updateRecordFwd(this.meta.columns);
|
|
1684
|
+
const updateRecord: Record<string, Expr> = {};
|
|
1685
|
+
for (const [key, value] of Object.entries(updateQrRecord)) {
|
|
1686
|
+
updateRecord[key] = expr.toExpr(value);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// insertRecord Generate (updateRecordRaw를 두 번째 인자로)
|
|
1690
|
+
const insertRecordRaw = insertRecordFwd(updateQrRecord);
|
|
1691
|
+
const insertRecord = Object.fromEntries(
|
|
1692
|
+
Object.entries(insertRecordRaw).map(([key, value]) => [key, expr.toExpr(value)]),
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
return objClearUndefined({
|
|
1696
|
+
type: "upsert",
|
|
1697
|
+
table: this.meta.db.getQueryDefObjectName(from),
|
|
1698
|
+
existsSelectQuery,
|
|
1699
|
+
updateRecord,
|
|
1700
|
+
insertRecord,
|
|
1701
|
+
output: outputColumns
|
|
1702
|
+
? {
|
|
1703
|
+
columns: outputColumns,
|
|
1704
|
+
pkColNames: outputDef.pkColNames,
|
|
1705
|
+
aiColName: outputDef.aiColName,
|
|
1706
|
+
}
|
|
1707
|
+
: undefined,
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
//#endregion
|
|
1712
|
+
|
|
1713
|
+
//#region ========== DDL Helper ==========
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* FK constraint on/off (transaction 내 사용 가능)
|
|
1717
|
+
*/
|
|
1718
|
+
async switchFk(switch_: "on" | "off"): Promise<void> {
|
|
1719
|
+
const from = this.meta.from;
|
|
1720
|
+
if (!(from instanceof TableBuilder) && !(from instanceof ViewBuilder)) {
|
|
1721
|
+
throw new Error(
|
|
1722
|
+
"switchFk는 TableBuilder 또는 ViewBuilder 기반 queryable에서만 사용할 수 있습니다.",
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
await this.meta.db.switchFk(this.meta.db.getQueryDefObjectName(from), switch_);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
//#endregion
|
|
1729
|
+
|
|
1730
|
+
//#region ========== CUD 공통 ==========
|
|
1731
|
+
|
|
1732
|
+
private _getCudOutputDef(): {
|
|
1733
|
+
pkColNames: string[];
|
|
1734
|
+
aiColName?: string;
|
|
1735
|
+
} {
|
|
1736
|
+
const from = this.meta.from;
|
|
1737
|
+
|
|
1738
|
+
if (from instanceof TableBuilder) {
|
|
1739
|
+
if (from.meta.columns == null) {
|
|
1740
|
+
throw new Error(`Table '${from.meta.name}' has no Column definition.`);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
let aiColName: string | undefined;
|
|
1744
|
+
for (const [key, col] of Object.entries(from.meta.columns as ColumnBuilderRecord)) {
|
|
1745
|
+
if (col.meta.autoIncrement) {
|
|
1746
|
+
aiColName = key;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
return {
|
|
1751
|
+
pkColNames: from.meta.primaryKey ?? [],
|
|
1752
|
+
aiColName,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
throw new Error("CUD operations can only be used on TableBuilder-based queryables.");
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
//#endregion
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
//#region ========== Helper Functions ==========
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* FK column 배열과 대상 Table의 PK를 매칭하여 PK column명 배열을 return
|
|
1766
|
+
*
|
|
1767
|
+
* @param fkCols - FK column명 array
|
|
1768
|
+
* @param targetTable - 참조 대상 Table builder
|
|
1769
|
+
* @returns 매칭된 PK column명 array
|
|
1770
|
+
* @throws FK/PK column 수 불일치 시
|
|
1771
|
+
*/
|
|
1772
|
+
export function getMatchedPrimaryKeys(
|
|
1773
|
+
fkCols: string[],
|
|
1774
|
+
targetTable: TableBuilder<any, any>,
|
|
1775
|
+
): string[] {
|
|
1776
|
+
const pk = targetTable.meta.primaryKey;
|
|
1777
|
+
if (pk == null || fkCols.length !== pk.length) {
|
|
1778
|
+
throw new Error(
|
|
1779
|
+
`FK/PK column 개수가 일치하지 않습니다 (대상: ${targetTable.meta.name}, FK: ${fkCols.length}개, PK: ${pk?.length ?? 0}개)`,
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
return pk;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* 중첩 columns structure를 새 alias로 Transform하는 공용 헬퍼
|
|
1787
|
+
*
|
|
1788
|
+
* Subquery/JOIN 시 기존 alias를 새 alias로 Transform하면서,
|
|
1789
|
+
* 중첩 키(posts.userId)는 평면화된 키로 유지한다.
|
|
1790
|
+
*
|
|
1791
|
+
* 예: posts[0].userId column의 경로가 ["T1.posts", "userId"]인 경우,
|
|
1792
|
+
* 새 alias "T2"로 Transform하면 ["T2", "posts.userId"]가 된다.
|
|
1793
|
+
*
|
|
1794
|
+
* @param columns - Transform할 column 레코드
|
|
1795
|
+
* @param alias - 새 Table alias (예: "T2")
|
|
1796
|
+
* @param keyPrefix - 현재 중첩 경로 (recursive 호출용, Default value "")
|
|
1797
|
+
* @returns Transform된 column 레코드
|
|
1798
|
+
*/
|
|
1799
|
+
function transformColumnsAlias<TRecord extends DataRecord>(
|
|
1800
|
+
columns: QueryableRecord<TRecord>,
|
|
1801
|
+
alias: string,
|
|
1802
|
+
keyPrefix: string = "",
|
|
1803
|
+
): QueryableRecord<TRecord> {
|
|
1804
|
+
const result: Record<string, unknown> = {};
|
|
1805
|
+
|
|
1806
|
+
for (const [key, value] of Object.entries(columns as Record<string, unknown>)) {
|
|
1807
|
+
const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
|
|
1808
|
+
|
|
1809
|
+
if (value instanceof ExprUnit) {
|
|
1810
|
+
result[key] = expr.col(value.dataType, alias, fullKey);
|
|
1811
|
+
} else if (Array.isArray(value)) {
|
|
1812
|
+
if (value.length > 0) {
|
|
1813
|
+
result[key] = [
|
|
1814
|
+
transformColumnsAlias(value[0] as QueryableRecord<DataRecord>, alias, fullKey),
|
|
1815
|
+
];
|
|
1816
|
+
}
|
|
1817
|
+
} else if (typeof value === "object" && value != null) {
|
|
1818
|
+
result[key] = transformColumnsAlias(value as QueryableRecord<DataRecord>, alias, fullKey);
|
|
1819
|
+
} else {
|
|
1820
|
+
result[key] = value;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
return result as QueryableRecord<TRecord>;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
//#endregion
|
|
1828
|
+
|
|
1829
|
+
//#region ========== Types ==========
|
|
1830
|
+
|
|
1831
|
+
interface QueryableMeta<TData extends DataRecord> {
|
|
1832
|
+
db: DbContextBase;
|
|
1833
|
+
from?:
|
|
1834
|
+
| TableBuilder<any, any>
|
|
1835
|
+
| ViewBuilder<any, any, any>
|
|
1836
|
+
| Queryable<any, any>
|
|
1837
|
+
| Queryable<TData, any>[]
|
|
1838
|
+
| string;
|
|
1839
|
+
as: string;
|
|
1840
|
+
columns: QueryableRecord<TData>;
|
|
1841
|
+
isCustomColumns?: boolean;
|
|
1842
|
+
distinct?: boolean;
|
|
1843
|
+
top?: number;
|
|
1844
|
+
lock?: boolean;
|
|
1845
|
+
where?: WhereExprUnit[];
|
|
1846
|
+
joins?: QueryableMetaJoin[];
|
|
1847
|
+
orderBy?: [ExprUnit<ColumnPrimitive>, ("ASC" | "DESC")?][];
|
|
1848
|
+
limit?: [number, number];
|
|
1849
|
+
groupBy?: ExprUnit<ColumnPrimitive>[];
|
|
1850
|
+
having?: WhereExprUnit[];
|
|
1851
|
+
with?: { name: string; base: Queryable<any, any>; recursive: Queryable<any, any> };
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
interface QueryableMetaJoin {
|
|
1855
|
+
queryable: Queryable<any, any>;
|
|
1856
|
+
isSingle: boolean;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
export type QueryableRecord<TData extends DataRecord> = {
|
|
1860
|
+
[K in keyof TData]: TData[K] extends ColumnPrimitive
|
|
1861
|
+
? ExprUnit<TData[K]>
|
|
1862
|
+
: TData[K] extends (infer U)[]
|
|
1863
|
+
? U extends DataRecord
|
|
1864
|
+
? QueryableRecord<U>[]
|
|
1865
|
+
: never
|
|
1866
|
+
: TData[K] extends (infer U)[] | undefined
|
|
1867
|
+
? U extends DataRecord
|
|
1868
|
+
? NullableQueryableRecord<U>[] | undefined
|
|
1869
|
+
: never
|
|
1870
|
+
: TData[K] extends DataRecord
|
|
1871
|
+
? QueryableRecord<TData[K]>
|
|
1872
|
+
: TData[K] extends DataRecord | undefined
|
|
1873
|
+
? NullableQueryableRecord<Exclude<TData[K], undefined>> | undefined
|
|
1874
|
+
: never;
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
export type QueryableWriteRecord<TData> = {
|
|
1878
|
+
[K in keyof TData]: TData[K] extends ColumnPrimitive ? ExprInput<TData[K]> : never;
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
export type NullableQueryableRecord<TData extends DataRecord> = {
|
|
1882
|
+
// Primitive — always | undefined (LEFT JOIN NULL propagation)
|
|
1883
|
+
[K in keyof TData]: TData[K] extends ColumnPrimitive
|
|
1884
|
+
? ExprUnit<TData[K] | undefined>
|
|
1885
|
+
: TData[K] extends (infer U)[]
|
|
1886
|
+
? U extends DataRecord
|
|
1887
|
+
? NullableQueryableRecord<U>[]
|
|
1888
|
+
: never
|
|
1889
|
+
: TData[K] extends (infer U)[] | undefined
|
|
1890
|
+
? U extends DataRecord
|
|
1891
|
+
? NullableQueryableRecord<U>[] | undefined
|
|
1892
|
+
: never
|
|
1893
|
+
: TData[K] extends DataRecord
|
|
1894
|
+
? NullableQueryableRecord<TData[K]>
|
|
1895
|
+
: TData[K] extends DataRecord | undefined
|
|
1896
|
+
? NullableQueryableRecord<Exclude<TData[K], undefined>> | undefined
|
|
1897
|
+
: never;
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* QueryableRecord에서 DataRecord로 역transform
|
|
1902
|
+
*
|
|
1903
|
+
* ExprUnit<T>를 T로, 중첩 object/배열을 재귀적으로 풀어냄
|
|
1904
|
+
*/
|
|
1905
|
+
export type UnwrapQueryableRecord<R> = {
|
|
1906
|
+
[K in keyof R]: R[K] extends ExprUnit<infer T>
|
|
1907
|
+
? T
|
|
1908
|
+
: NonNullable<R[K]> extends (infer U)[]
|
|
1909
|
+
? U extends Record<string, any>
|
|
1910
|
+
? UnwrapQueryableRecord<U>[] | Extract<R[K], undefined>
|
|
1911
|
+
: never
|
|
1912
|
+
: NonNullable<R[K]> extends Record<string, any>
|
|
1913
|
+
? UnwrapQueryableRecord<NonNullable<R[K]>> | Extract<R[K], undefined>
|
|
1914
|
+
: never;
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
//#region ========== PathProxy - include용 type 안전 경로 builder ==========
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* include()에서 relationship 경로를 type 안전하게 지정하기 위한 Proxy type
|
|
1921
|
+
* ColumnPrimitive가 아닌 필드(FK, FKT relationship)만 access 가능
|
|
1922
|
+
*
|
|
1923
|
+
* @example
|
|
1924
|
+
* ```typescript
|
|
1925
|
+
* // item.user.company access 시 내부적으로 ["user", "company"] 경로 수집
|
|
1926
|
+
* db.post.include(item => item.user.company)
|
|
1927
|
+
*
|
|
1928
|
+
* // item.title은 string(ColumnPrimitive)이므로 컴파일 에러
|
|
1929
|
+
* db.post.include(item => item.title) // ❌ 에러
|
|
1930
|
+
* ```
|
|
1931
|
+
*/
|
|
1932
|
+
/**
|
|
1933
|
+
* 배열이면 요소 type 추출
|
|
1934
|
+
*/
|
|
1935
|
+
type UnwrapArray<TArray> = TArray extends (infer TElement)[] ? TElement : TArray;
|
|
1936
|
+
|
|
1937
|
+
const PATH_SYMBOL = Symbol("path");
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* include()용 type 안전 경로 프록시
|
|
1941
|
+
*/
|
|
1942
|
+
export type PathProxy<TObject> = {
|
|
1943
|
+
[K in keyof TObject as TObject[K] extends ColumnPrimitive ? never : K]-?: PathProxy<
|
|
1944
|
+
UnwrapArray<TObject[K]>
|
|
1945
|
+
>;
|
|
1946
|
+
} & { readonly [PATH_SYMBOL]: string[] };
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* PathProxy instance Generate
|
|
1950
|
+
* Proxy를 사용하여 프로퍼티 접근을 가로채고 경로를 수집
|
|
1951
|
+
*/
|
|
1952
|
+
function createPathProxy<TObject>(path: string[] = []): PathProxy<TObject> {
|
|
1953
|
+
return new Proxy({} as PathProxy<TObject>, {
|
|
1954
|
+
get(_, prop: string | symbol) {
|
|
1955
|
+
if (prop === PATH_SYMBOL) return path;
|
|
1956
|
+
if (typeof prop === "symbol") return undefined;
|
|
1957
|
+
return createPathProxy<unknown>([...path, prop]);
|
|
1958
|
+
},
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
//#endregion
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Table 또는 View에 대한 Queryable factory 함수를 Generate
|
|
1966
|
+
*
|
|
1967
|
+
* DbContext에서 Table/View별 getter를 정의할 때 사용
|
|
1968
|
+
*
|
|
1969
|
+
* @param db - DbContext instance
|
|
1970
|
+
* @param tableOrView - TableBuilder 또는 ViewBuilder instance
|
|
1971
|
+
* @param as - alias 지정 (selection, 미지정 시 automatic Create)
|
|
1972
|
+
* @returns Queryable을 반환하는 factory function
|
|
1973
|
+
*
|
|
1974
|
+
* @example
|
|
1975
|
+
* ```typescript
|
|
1976
|
+
* class AppDbContext extends DbContext {
|
|
1977
|
+
* // 호출 시마다 새로운 alias 할당
|
|
1978
|
+
* user = queryable(this, User);
|
|
1979
|
+
*
|
|
1980
|
+
* // 사용 예시
|
|
1981
|
+
* async getActiveUsers() {
|
|
1982
|
+
* return this.user()
|
|
1983
|
+
* .where((u) => [expr.eq(u.isActive, true)])
|
|
1984
|
+
* .result();
|
|
1985
|
+
* }
|
|
1986
|
+
* }
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
export function queryable<TBuilder extends TableBuilder<any, any> | ViewBuilder<any, any, any>>(
|
|
1990
|
+
db: DbContextBase,
|
|
1991
|
+
tableOrView: TBuilder,
|
|
1992
|
+
as?: string,
|
|
1993
|
+
): () => Queryable<TBuilder["$infer"], TBuilder extends TableBuilder<any, any> ? TBuilder : never> {
|
|
1994
|
+
return () => {
|
|
1995
|
+
// as가 명시되지 않으면 db.getNextAlias() 사용 (카운터 증가)
|
|
1996
|
+
// as가 명시되면 그대로 사용 (카운터 증가 안함)
|
|
1997
|
+
const finalAs = as ?? db.getNextAlias();
|
|
1998
|
+
|
|
1999
|
+
// TableBuilder + columns
|
|
2000
|
+
if (tableOrView instanceof TableBuilder && tableOrView.meta.columns != null) {
|
|
2001
|
+
const columnDefs = tableOrView.meta.columns as ColumnBuilderRecord;
|
|
2002
|
+
|
|
2003
|
+
return new Queryable({
|
|
2004
|
+
db,
|
|
2005
|
+
from: tableOrView,
|
|
2006
|
+
as: finalAs,
|
|
2007
|
+
columns: Object.fromEntries(
|
|
2008
|
+
Object.entries(columnDefs).map(([key, colDef]) => [
|
|
2009
|
+
key,
|
|
2010
|
+
expr.col(colDef.meta.type, finalAs, key),
|
|
2011
|
+
]),
|
|
2012
|
+
),
|
|
2013
|
+
}) as any;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// ViewBuilder + viewFn
|
|
2017
|
+
if (tableOrView instanceof ViewBuilder && tableOrView.meta.viewFn != null) {
|
|
2018
|
+
const baseQr = tableOrView.meta.viewFn(db);
|
|
2019
|
+
|
|
2020
|
+
// TFrom을 ViewBuilder로 설정하여 return
|
|
2021
|
+
return new Queryable({
|
|
2022
|
+
db,
|
|
2023
|
+
from: tableOrView,
|
|
2024
|
+
as: finalAs,
|
|
2025
|
+
columns: transformColumnsAlias(baseQr.meta.columns, finalAs),
|
|
2026
|
+
}) as any;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
throw new Error(`Invalid Table/View Metadata: ${tableOrView.meta.name}`);
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
//#endregion
|