@apisr/drizzle-model 0.0.3 → 2.0.0
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/DISCLAIMER.md +5 -0
- package/TODO.md +8 -61
- package/package.json +2 -1
- package/src/core/dialect.ts +81 -0
- package/src/core/index.ts +24 -0
- package/src/core/query/error.ts +15 -0
- package/src/core/query/joins.ts +596 -0
- package/src/core/query/projection.ts +136 -0
- package/src/core/query/where.ts +449 -0
- package/src/core/result.ts +297 -0
- package/src/core/runtime.ts +612 -0
- package/src/core/transform.ts +119 -0
- package/src/model/builder.ts +40 -6
- package/src/model/config.ts +9 -9
- package/src/model/format.ts +20 -8
- package/src/model/methods/exclude.ts +1 -7
- package/src/model/methods/return.ts +11 -11
- package/src/model/methods/select.ts +2 -8
- package/src/model/model.ts +10 -16
- package/src/model/query/error.ts +1 -0
- package/src/model/result.ts +134 -21
- package/src/types.ts +38 -0
- package/tests/base/count.test.ts +47 -0
- package/tests/base/delete.test.ts +90 -0
- package/tests/base/find.test.ts +209 -0
- package/tests/base/insert.test.ts +152 -0
- package/tests/base/safe.test.ts +91 -0
- package/tests/base/update.test.ts +88 -0
- package/tests/base/upsert.test.ts +121 -0
- package/tests/base.ts +21 -0
- package/tests/snippets/x-1.ts +22 -0
- package/src/model/core/joins.ts +0 -364
- package/src/model/core/projection.ts +0 -61
- package/src/model/core/runtime.ts +0 -330
- package/src/model/core/thenable.ts +0 -94
- package/src/model/core/transform.ts +0 -65
- package/src/model/core/where.ts +0 -249
- package/src/model/core/with.ts +0 -28
- package/tests/builder-v2-mysql.type-test.ts +0 -51
- package/tests/builder-v2.type-test.ts +0 -336
- package/tests/builder.test.ts +0 -63
- package/tests/find.test.ts +0 -166
- package/tests/insert.test.ts +0 -247
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import type { ModelDialect } from "../model/dialect.ts";
|
|
2
|
+
import type { ModelOptions } from "../model/options.ts";
|
|
3
|
+
import { DialectHelper } from "./dialect.ts";
|
|
4
|
+
import { JoinExecutor } from "./query/joins.ts";
|
|
5
|
+
import { ProjectionBuilder } from "./query/projection.ts";
|
|
6
|
+
import { WhereCompiler } from "./query/where.ts";
|
|
7
|
+
import {
|
|
8
|
+
type MutateKind,
|
|
9
|
+
MutateResult,
|
|
10
|
+
type MutateState,
|
|
11
|
+
QueryResult,
|
|
12
|
+
type QueryState,
|
|
13
|
+
} from "./result.ts";
|
|
14
|
+
import { ResultTransformer } from "./transform.ts";
|
|
15
|
+
|
|
16
|
+
/** Generic record type used throughout the runtime. */
|
|
17
|
+
type AnyRecord = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Configuration
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration required to construct a {@link ModelRuntime}.
|
|
25
|
+
*
|
|
26
|
+
* Carries all the static metadata about the table, schema, relations,
|
|
27
|
+
* dialect and user-defined options.
|
|
28
|
+
*/
|
|
29
|
+
export interface ModelRuntimeConfig {
|
|
30
|
+
/** The Drizzle database instance. */
|
|
31
|
+
db: unknown;
|
|
32
|
+
/** The SQL dialect of the database. */
|
|
33
|
+
dialect: ModelDialect;
|
|
34
|
+
/** User-defined model options (format, methods, where, …). */
|
|
35
|
+
options: ModelOptions<never, never, never, never>;
|
|
36
|
+
/** The Drizzle relations metadata map. */
|
|
37
|
+
relations: Record<string, AnyRecord>;
|
|
38
|
+
/** The full schema map (`{ tableName: drizzleTable }`). */
|
|
39
|
+
schema: Record<string, AnyRecord>;
|
|
40
|
+
/** The name of the table this model represents. */
|
|
41
|
+
tableName: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// ModelRuntime
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The runtime implementation behind every model instance.
|
|
50
|
+
*
|
|
51
|
+
* Exposes query methods (`findMany`, `findFirst`), mutation methods
|
|
52
|
+
* (`insert`, `update`, `delete`, `upsert`), and lifecycle helpers
|
|
53
|
+
* (`where`, `extend`, `db`, `include`).
|
|
54
|
+
*
|
|
55
|
+
* Internally delegates to specialised helpers:
|
|
56
|
+
* - {@link WhereCompiler} — compiles where clauses.
|
|
57
|
+
* - {@link ProjectionBuilder} — builds column projections.
|
|
58
|
+
* - {@link JoinExecutor} — executes relation joins.
|
|
59
|
+
* - {@link ResultTransformer} — applies post-query transforms.
|
|
60
|
+
*
|
|
61
|
+
* Each call to `.where()` returns a **new** runtime with the additional
|
|
62
|
+
* filter applied, keeping the original immutable.
|
|
63
|
+
*/
|
|
64
|
+
export class ModelRuntime {
|
|
65
|
+
/** Static configuration for this model. */
|
|
66
|
+
private readonly config: ModelRuntimeConfig;
|
|
67
|
+
|
|
68
|
+
/** The current where filter applied via `.where()`. */
|
|
69
|
+
private readonly currentWhere: unknown;
|
|
70
|
+
|
|
71
|
+
// Shared helpers (stateless, safe to reuse)
|
|
72
|
+
private readonly whereCompiler: WhereCompiler;
|
|
73
|
+
private readonly projection: ProjectionBuilder;
|
|
74
|
+
private readonly joinExecutor: JoinExecutor;
|
|
75
|
+
private readonly transformer: ResultTransformer;
|
|
76
|
+
private readonly dialectHelper: DialectHelper;
|
|
77
|
+
|
|
78
|
+
constructor(config: ModelRuntimeConfig, currentWhere?: unknown) {
|
|
79
|
+
this.config = config;
|
|
80
|
+
this.currentWhere = currentWhere;
|
|
81
|
+
|
|
82
|
+
this.dialectHelper = new DialectHelper(config.dialect);
|
|
83
|
+
this.whereCompiler = new WhereCompiler();
|
|
84
|
+
this.projection = new ProjectionBuilder();
|
|
85
|
+
this.joinExecutor = new JoinExecutor(this.dialectHelper);
|
|
86
|
+
this.transformer = new ResultTransformer();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Public: identity & meta
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/** Model discriminator tag. */
|
|
94
|
+
get $model(): "model" {
|
|
95
|
+
return "model";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The name of the table this model is bound to. */
|
|
99
|
+
get $modelName(): string {
|
|
100
|
+
return this.config.tableName;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The user-defined format function, if any. */
|
|
104
|
+
get $format(): unknown {
|
|
105
|
+
return this.config.options.format;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Public: filtering
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns a new runtime with an additional where filter.
|
|
114
|
+
*
|
|
115
|
+
* The new filter is AND-ed with any existing model-level where clause
|
|
116
|
+
* at execution time.
|
|
117
|
+
*
|
|
118
|
+
* @param value - A where clause (object, SQL, or model reference).
|
|
119
|
+
* @returns A new {@link ModelRuntime} with the filter applied.
|
|
120
|
+
*/
|
|
121
|
+
where(value: unknown): ModelRuntime {
|
|
122
|
+
return new ModelRuntime(this.config, value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Public: lifecycle
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns the `.with()` value as-is.
|
|
131
|
+
*
|
|
132
|
+
* This is a pass-through used at the type level to allow
|
|
133
|
+
* `model.include({ posts: true })` syntax.
|
|
134
|
+
*
|
|
135
|
+
* @param value - The relation include descriptor.
|
|
136
|
+
* @returns The same value, unchanged.
|
|
137
|
+
*/
|
|
138
|
+
include(value: unknown): unknown {
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates a new runtime with merged options.
|
|
144
|
+
*
|
|
145
|
+
* Useful for extending a base model with additional format functions,
|
|
146
|
+
* methods, or default where clauses.
|
|
147
|
+
*
|
|
148
|
+
* @param nextOptions - Partial options to merge.
|
|
149
|
+
* @returns A new {@link ModelRuntime} with the merged configuration.
|
|
150
|
+
*/
|
|
151
|
+
extend(
|
|
152
|
+
nextOptions: Partial<ModelOptions<never, never, never, never>>
|
|
153
|
+
): ModelRuntime {
|
|
154
|
+
return new ModelRuntime({
|
|
155
|
+
...this.config,
|
|
156
|
+
options: {
|
|
157
|
+
...this.config.options,
|
|
158
|
+
...nextOptions,
|
|
159
|
+
methods: {
|
|
160
|
+
...(nextOptions.methods ?? {}),
|
|
161
|
+
...(this.config.options.methods ?? {}),
|
|
162
|
+
},
|
|
163
|
+
format: nextOptions.format ?? this.config.options.format,
|
|
164
|
+
} as ModelOptions<never, never, never, never>,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Creates a new runtime bound to a different database instance.
|
|
170
|
+
*
|
|
171
|
+
* @param db - The new Drizzle database instance.
|
|
172
|
+
* @returns A new {@link ModelRuntime} using the given database.
|
|
173
|
+
*/
|
|
174
|
+
db(db: unknown): ModelRuntime {
|
|
175
|
+
return new ModelRuntime({ ...this.config, db });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Public: queries
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Returns a thenable that resolves to an array of matching rows.
|
|
184
|
+
*
|
|
185
|
+
* Supports chaining: `.select()`, `.exclude()`, `.with()`, `.raw()`.
|
|
186
|
+
*
|
|
187
|
+
* @returns A {@link QueryResult} that can be awaited or further chained.
|
|
188
|
+
*/
|
|
189
|
+
findMany(): QueryResult<unknown> {
|
|
190
|
+
const runner = async (qState: QueryState): Promise<unknown> => {
|
|
191
|
+
const table = this.getTable();
|
|
192
|
+
const whereSql = this.buildEffectiveWhere(table);
|
|
193
|
+
|
|
194
|
+
let result: unknown;
|
|
195
|
+
|
|
196
|
+
if (qState.with) {
|
|
197
|
+
result = await this.joinExecutor.execute({
|
|
198
|
+
db: this.config.db,
|
|
199
|
+
schema: this.config.schema,
|
|
200
|
+
relations: this.config.relations,
|
|
201
|
+
baseTableName: this.config.tableName,
|
|
202
|
+
baseTable: table,
|
|
203
|
+
whereSql,
|
|
204
|
+
withValue: qState.with as AnyRecord,
|
|
205
|
+
limitOne: false,
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
const { selectMap } = this.projection.build(
|
|
209
|
+
table,
|
|
210
|
+
qState.select,
|
|
211
|
+
qState.exclude
|
|
212
|
+
);
|
|
213
|
+
const db = this.config.db as AnyRecord;
|
|
214
|
+
let query = (db.select as (m: AnyRecord) => AnyRecord)(selectMap);
|
|
215
|
+
query = (
|
|
216
|
+
query as AnyRecord & { from: (t: AnyRecord) => AnyRecord }
|
|
217
|
+
).from(table);
|
|
218
|
+
|
|
219
|
+
if (whereSql) {
|
|
220
|
+
query = (
|
|
221
|
+
query as AnyRecord & { where: (w: unknown) => AnyRecord }
|
|
222
|
+
).where(whereSql);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
result = await (query as unknown as PromiseLike<unknown>);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return this.applyPostQueryTransforms(result, qState);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return new QueryResult({} as QueryState, runner);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns a thenable that resolves to the first matching row (or `undefined`).
|
|
236
|
+
*
|
|
237
|
+
* Supports chaining: `.select()`, `.exclude()`, `.with()`, `.raw()`.
|
|
238
|
+
*
|
|
239
|
+
* @returns A {@link QueryResult} that can be awaited or further chained.
|
|
240
|
+
*/
|
|
241
|
+
findFirst(): QueryResult<unknown> {
|
|
242
|
+
const runner = async (qState: QueryState): Promise<unknown> => {
|
|
243
|
+
const table = this.getTable();
|
|
244
|
+
const whereSql = this.buildEffectiveWhere(table);
|
|
245
|
+
|
|
246
|
+
let result: unknown;
|
|
247
|
+
|
|
248
|
+
if (qState.with) {
|
|
249
|
+
result = await this.joinExecutor.execute({
|
|
250
|
+
db: this.config.db,
|
|
251
|
+
schema: this.config.schema,
|
|
252
|
+
relations: this.config.relations,
|
|
253
|
+
baseTableName: this.config.tableName,
|
|
254
|
+
baseTable: table,
|
|
255
|
+
whereSql,
|
|
256
|
+
withValue: qState.with as AnyRecord,
|
|
257
|
+
limitOne: true,
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
const { selectMap } = this.projection.build(
|
|
261
|
+
table,
|
|
262
|
+
qState.select,
|
|
263
|
+
qState.exclude
|
|
264
|
+
);
|
|
265
|
+
const db = this.config.db as AnyRecord;
|
|
266
|
+
let query = (db.select as (m: AnyRecord) => AnyRecord)(selectMap);
|
|
267
|
+
query = (
|
|
268
|
+
query as AnyRecord & { from: (t: AnyRecord) => AnyRecord }
|
|
269
|
+
).from(table);
|
|
270
|
+
|
|
271
|
+
if (whereSql) {
|
|
272
|
+
query = (
|
|
273
|
+
query as AnyRecord & { where: (w: unknown) => AnyRecord }
|
|
274
|
+
).where(whereSql);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
query = (
|
|
278
|
+
query as AnyRecord & { limit: (n: number) => AnyRecord }
|
|
279
|
+
).limit(1);
|
|
280
|
+
const rows = (await (query as unknown as PromiseLike<
|
|
281
|
+
unknown[]
|
|
282
|
+
>)) as unknown[];
|
|
283
|
+
result = rows[0];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return this.applyPostQueryTransforms(result, qState);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return new QueryResult({} as QueryState, runner);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Returns a promise that resolves to the number of matching rows.
|
|
294
|
+
*
|
|
295
|
+
* Respects the effective where clause (model-level + `.where()`).
|
|
296
|
+
*
|
|
297
|
+
* @returns A `Promise<number>` with the row count.
|
|
298
|
+
*/
|
|
299
|
+
async count(): Promise<number> {
|
|
300
|
+
const table = this.getTable();
|
|
301
|
+
const whereSql = this.buildEffectiveWhere(table);
|
|
302
|
+
|
|
303
|
+
const db = this.config.db as AnyRecord;
|
|
304
|
+
const { count: countFn } = await import("drizzle-orm");
|
|
305
|
+
|
|
306
|
+
let query = (db.select as (m: AnyRecord) => AnyRecord)({
|
|
307
|
+
count: countFn(),
|
|
308
|
+
});
|
|
309
|
+
query = (query as AnyRecord & { from: (t: AnyRecord) => AnyRecord }).from(
|
|
310
|
+
table
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (whereSql) {
|
|
314
|
+
query = (query as AnyRecord & { where: (w: unknown) => AnyRecord }).where(
|
|
315
|
+
whereSql
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const rows = (await (query as unknown as PromiseLike<
|
|
320
|
+
AnyRecord[]
|
|
321
|
+
>)) as AnyRecord[];
|
|
322
|
+
return (rows[0]?.count as number) ?? 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Public: mutations
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Inserts one or more rows into the table.
|
|
331
|
+
*
|
|
332
|
+
* @param value - The row(s) to insert.
|
|
333
|
+
* @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
|
|
334
|
+
*/
|
|
335
|
+
insert(value: unknown): MutateResult<unknown> {
|
|
336
|
+
const runner = async (mState: MutateState): Promise<unknown> => {
|
|
337
|
+
const table = this.getTable();
|
|
338
|
+
const db = this.config.db as AnyRecord;
|
|
339
|
+
const query = (db.insert as (t: AnyRecord) => AnyRecord)(table);
|
|
340
|
+
const withValues = (
|
|
341
|
+
query as AnyRecord & { values: (v: unknown) => unknown }
|
|
342
|
+
).values(mState.value);
|
|
343
|
+
|
|
344
|
+
let result = await this.execReturning(withValues, mState);
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
!(mState.hasReturn || Array.isArray(mState.value)) &&
|
|
348
|
+
Array.isArray(result)
|
|
349
|
+
) {
|
|
350
|
+
result = (result as unknown[])[0];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return this.applyPostMutateTransforms(result, mState);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return new MutateResult({ kind: "insert" as MutateKind, value }, runner);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Updates rows matching the current where clause.
|
|
361
|
+
*
|
|
362
|
+
* @param value - The partial row data to set.
|
|
363
|
+
* @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
|
|
364
|
+
*/
|
|
365
|
+
update(value: unknown): MutateResult<unknown> {
|
|
366
|
+
const runner = async (mState: MutateState): Promise<unknown> => {
|
|
367
|
+
const table = this.getTable();
|
|
368
|
+
const whereSql = this.buildEffectiveWhere(table);
|
|
369
|
+
const db = this.config.db as AnyRecord;
|
|
370
|
+
|
|
371
|
+
let query: unknown = (db.update as (t: AnyRecord) => AnyRecord)(table);
|
|
372
|
+
query = (query as AnyRecord & { set: (v: unknown) => unknown }).set(
|
|
373
|
+
mState.value
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (whereSql) {
|
|
377
|
+
query = (query as AnyRecord & { where: (w: unknown) => unknown }).where(
|
|
378
|
+
whereSql
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const result = await this.execReturning(query, mState);
|
|
383
|
+
return this.applyPostMutateTransforms(result, mState);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return new MutateResult({ kind: "update" as MutateKind, value }, runner);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Deletes rows matching the current where clause.
|
|
391
|
+
*
|
|
392
|
+
* @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
|
|
393
|
+
*/
|
|
394
|
+
delete(): MutateResult<unknown> {
|
|
395
|
+
const runner = async (mState: MutateState): Promise<unknown> => {
|
|
396
|
+
const table = this.getTable();
|
|
397
|
+
const whereSql = this.buildEffectiveWhere(table);
|
|
398
|
+
const db = this.config.db as AnyRecord;
|
|
399
|
+
|
|
400
|
+
let query: unknown = (db.delete as (t: AnyRecord) => unknown)(table);
|
|
401
|
+
|
|
402
|
+
if (whereSql) {
|
|
403
|
+
query = (query as AnyRecord & { where: (w: unknown) => unknown }).where(
|
|
404
|
+
whereSql
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const result = await this.execReturning(query, mState);
|
|
409
|
+
return this.applyPostMutateTransforms(result, mState);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return new MutateResult({ kind: "delete" as MutateKind }, runner);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Inserts a row, or updates it when a conflict is detected.
|
|
417
|
+
*
|
|
418
|
+
* The `value` must contain `insert`, `update`, and optionally `target`.
|
|
419
|
+
*
|
|
420
|
+
* @param value - The upsert descriptor.
|
|
421
|
+
* @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
|
|
422
|
+
*/
|
|
423
|
+
upsert(value: unknown): MutateResult<unknown> {
|
|
424
|
+
const runner = async (mState: MutateState): Promise<unknown> => {
|
|
425
|
+
const table = this.getTable();
|
|
426
|
+
const upsertValue = mState.value as AnyRecord;
|
|
427
|
+
|
|
428
|
+
const insertValues = upsertValue.insert;
|
|
429
|
+
const updateCfg = upsertValue.update;
|
|
430
|
+
const target = this.normalizeUpsertTarget(table, upsertValue.target);
|
|
431
|
+
|
|
432
|
+
let updateSet = updateCfg;
|
|
433
|
+
if (typeof updateCfg === "function") {
|
|
434
|
+
updateSet = (updateCfg as (ctx: AnyRecord) => unknown)({
|
|
435
|
+
excluded: (field: string) => (table as AnyRecord)[field],
|
|
436
|
+
inserted: (field: string) => (table as AnyRecord)[field],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const db = this.config.db as AnyRecord;
|
|
441
|
+
let query: unknown = (db.insert as (t: AnyRecord) => AnyRecord)(table);
|
|
442
|
+
query = (query as AnyRecord & { values: (v: unknown) => unknown }).values(
|
|
443
|
+
insertValues
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const queryRecord = query as AnyRecord;
|
|
447
|
+
if (typeof queryRecord.onConflictDoUpdate === "function") {
|
|
448
|
+
query = (queryRecord.onConflictDoUpdate as (cfg: AnyRecord) => unknown)(
|
|
449
|
+
{
|
|
450
|
+
target,
|
|
451
|
+
set: updateSet,
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const result = await this.execReturning(query, mState);
|
|
457
|
+
return this.applyPostMutateTransforms(result, mState);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return new MutateResult({ kind: "upsert" as MutateKind, value }, runner);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Public: method attachment
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Attaches user-defined methods from the model options to a target object.
|
|
469
|
+
*
|
|
470
|
+
* Each method is bound to `target` so that `this` inside the method
|
|
471
|
+
* refers to the model-like object.
|
|
472
|
+
*
|
|
473
|
+
* @param target - The object to attach methods to.
|
|
474
|
+
*/
|
|
475
|
+
attachMethods(target: AnyRecord): void {
|
|
476
|
+
const methods = this.config.options.methods;
|
|
477
|
+
if (!methods) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const [key, fn] of Object.entries(methods)) {
|
|
482
|
+
if (typeof fn === "function") {
|
|
483
|
+
target[key] = (fn as (...args: unknown[]) => unknown).bind(target);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Private: helpers
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Applies `returnFirst` and `omit` transforms to a mutation result.
|
|
494
|
+
*/
|
|
495
|
+
private applyPostMutateTransforms(
|
|
496
|
+
result: unknown,
|
|
497
|
+
mState: MutateState
|
|
498
|
+
): unknown {
|
|
499
|
+
let out = result;
|
|
500
|
+
|
|
501
|
+
if (mState.returnFirst && Array.isArray(out)) {
|
|
502
|
+
out = (out as unknown[])[0];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (mState.omit) {
|
|
506
|
+
out = this.transformer.applyExclude(out, mState.omit);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Retrieves the Drizzle table object for the configured table name.
|
|
514
|
+
*/
|
|
515
|
+
private getTable(): AnyRecord {
|
|
516
|
+
return this.config.schema[this.config.tableName] as AnyRecord;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Builds the effective where clause by merging the model-level where
|
|
521
|
+
* (from options) with the call-level where (from `.where()`).
|
|
522
|
+
*/
|
|
523
|
+
private buildEffectiveWhere(table: AnyRecord): unknown {
|
|
524
|
+
return this.whereCompiler.compileEffective(
|
|
525
|
+
table,
|
|
526
|
+
this.config.options.where,
|
|
527
|
+
this.currentWhere
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Applies select, exclude, and format transforms to the query result.
|
|
533
|
+
*/
|
|
534
|
+
private applyPostQueryTransforms(
|
|
535
|
+
result: unknown,
|
|
536
|
+
qState: QueryState
|
|
537
|
+
): unknown {
|
|
538
|
+
let out = result;
|
|
539
|
+
|
|
540
|
+
if (qState.select) {
|
|
541
|
+
out = this.transformer.applySelect(out, qState.select);
|
|
542
|
+
}
|
|
543
|
+
if (qState.exclude) {
|
|
544
|
+
out = this.transformer.applyExclude(out, qState.exclude);
|
|
545
|
+
}
|
|
546
|
+
if (!qState.raw) {
|
|
547
|
+
out = this.transformer.applyFormat(
|
|
548
|
+
out,
|
|
549
|
+
this.config.options.format as ((row: AnyRecord) => unknown) | undefined
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Executes the returning clause for a mutation query.
|
|
558
|
+
*
|
|
559
|
+
* Handles the dialect-specific differences:
|
|
560
|
+
* - Standard `.returning()` (PostgreSQL, SQLite).
|
|
561
|
+
* - `.$returningId()` (MySQL, SingleStore, CockroachDB).
|
|
562
|
+
*
|
|
563
|
+
* @param query - The built mutation query.
|
|
564
|
+
* @param mState - The mutation state (may contain `returnSelect`).
|
|
565
|
+
* @returns The mutation result.
|
|
566
|
+
*/
|
|
567
|
+
private async execReturning(
|
|
568
|
+
query: unknown,
|
|
569
|
+
mState: MutateState
|
|
570
|
+
): Promise<unknown> {
|
|
571
|
+
const queryRecord = query as AnyRecord;
|
|
572
|
+
|
|
573
|
+
if (typeof queryRecord.returning === "function") {
|
|
574
|
+
return mState.returnSelect
|
|
575
|
+
? await (
|
|
576
|
+
queryRecord.returning as (sel: AnyRecord) => PromiseLike<unknown>
|
|
577
|
+
)(mState.returnSelect)
|
|
578
|
+
: await (queryRecord.returning as () => PromiseLike<unknown>)();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (
|
|
582
|
+
this.dialectHelper.isReturningIdOnly() &&
|
|
583
|
+
typeof queryRecord.$returningId === "function"
|
|
584
|
+
) {
|
|
585
|
+
return await (queryRecord.$returningId as () => PromiseLike<unknown>)();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return await (query as PromiseLike<unknown>);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Normalizes the upsert conflict target.
|
|
593
|
+
*
|
|
594
|
+
* Converts string column names to their Drizzle column references,
|
|
595
|
+
* handling both single values and arrays.
|
|
596
|
+
*/
|
|
597
|
+
private normalizeUpsertTarget(table: AnyRecord, target: unknown): unknown {
|
|
598
|
+
if (!target) {
|
|
599
|
+
return target;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (typeof target === "string") {
|
|
603
|
+
return table[target] ?? target;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (Array.isArray(target)) {
|
|
607
|
+
return target.map((t) => (typeof t === "string" ? (table[t] ?? t) : t));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return target;
|
|
611
|
+
}
|
|
612
|
+
}
|