@apisr/drizzle-model 2.0.2 → 2.0.4

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.
@@ -0,0 +1,215 @@
1
+ //#region src/core/result.ts
2
+ /**
3
+ * A lazy result that implements `PromiseLike` so it can be `await`-ed.
4
+ *
5
+ * Execution is deferred until `.then()` is called, allowing the caller
6
+ * to chain modifiers (`.select()`, `.with()`, …) before the query runs.
7
+ *
8
+ * When `_safe` is `true`, execution errors are caught and the result
9
+ * is wrapped in a {@link SafeResult} discriminated union.
10
+ *
11
+ * @typeParam T - The resolved result type.
12
+ */
13
+ var ThenableResult = class {
14
+ /** The deferred execution function. */
15
+ _execute;
16
+ /** When `true`, wraps the result in `{ data, error }`. */
17
+ _safe;
18
+ constructor(execute, safe = false) {
19
+ this._execute = execute;
20
+ this._safe = safe;
21
+ }
22
+ /**
23
+ * Implements the `PromiseLike` interface.
24
+ *
25
+ * Triggers the deferred execution and forwards to the native
26
+ * `Promise.then()`. When `_safe` is enabled, catches errors and
27
+ * resolves to `{ data, error }` instead of rejecting.
28
+ */
29
+ then(onfulfilled, onrejected) {
30
+ if (!this._safe) return this._execute().then(onfulfilled, onrejected);
31
+ return this._execute().then((data) => ({
32
+ data,
33
+ error: void 0
34
+ })).catch((error) => ({
35
+ data: void 0,
36
+ error
37
+ })).then(onfulfilled, onrejected);
38
+ }
39
+ };
40
+ /**
41
+ * A thenable query result that supports chaining query modifiers.
42
+ *
43
+ * Each modifier returns a **new** `QueryResult` with the updated state,
44
+ * keeping the original immutable.
45
+ *
46
+ * @typeParam T - The resolved result type.
47
+ */
48
+ var QueryResult = class QueryResult extends ThenableResult {
49
+ /** The current accumulated query state. */
50
+ state;
51
+ /** The runner function that executes the query with the given state. */
52
+ runner;
53
+ constructor(state, runner) {
54
+ super(() => runner(state), state.safe);
55
+ this.state = state;
56
+ this.runner = runner;
57
+ }
58
+ /**
59
+ * Includes related entities via LEFT JOINs.
60
+ *
61
+ * @param value - A relation selection map (e.g. `{ posts: true }`).
62
+ * @returns A new `QueryResult` with the `.with()` state applied.
63
+ */
64
+ with(value) {
65
+ return new QueryResult({
66
+ ...this.state,
67
+ with: value
68
+ }, this.runner);
69
+ }
70
+ /**
71
+ * Controls which columns appear in the SQL SELECT clause (whitelist).
72
+ *
73
+ * This affects the query itself, not just the result.
74
+ * Equivalent to `db.select({ col: table.col }).from(table)`.
75
+ *
76
+ * @param value - A map of `{ columnName: true }`.
77
+ * @returns A new `QueryResult` with the `.select()` state applied.
78
+ */
79
+ select(value) {
80
+ return new QueryResult({
81
+ ...this.state,
82
+ select: value
83
+ }, this.runner);
84
+ }
85
+ /**
86
+ * Controls which columns are excluded from the SQL SELECT clause (blacklist).
87
+ *
88
+ * This affects the query itself, not just the result.
89
+ * All columns except the listed ones will be fetched.
90
+ *
91
+ * @param value - A map of `{ columnName: true }`.
92
+ * @returns A new `QueryResult` with the `.exclude()` state applied.
93
+ */
94
+ exclude(value) {
95
+ return new QueryResult({
96
+ ...this.state,
97
+ exclude: value
98
+ }, this.runner);
99
+ }
100
+ /**
101
+ * Disables format transformations for this query.
102
+ *
103
+ * @returns A new `QueryResult` with `raw` set to `true`.
104
+ */
105
+ raw() {
106
+ return new QueryResult({
107
+ ...this.state,
108
+ raw: true
109
+ }, this.runner);
110
+ }
111
+ /**
112
+ * Wraps the result in a `{ data, error }` discriminated union.
113
+ *
114
+ * When the query succeeds, resolves to `{ data: T, error: undefined }`.
115
+ * When it fails, resolves to `{ data: undefined, error: unknown }`
116
+ * instead of rejecting.
117
+ *
118
+ * @returns A new `QueryResult` with safe error handling enabled.
119
+ */
120
+ safe() {
121
+ return new QueryResult({
122
+ ...this.state,
123
+ safe: true
124
+ }, this.runner);
125
+ }
126
+ /**
127
+ * Returns the current query state for debugging purposes.
128
+ *
129
+ * @returns The accumulated {@link QueryState}.
130
+ */
131
+ debug() {
132
+ return this.state;
133
+ }
134
+ };
135
+ /**
136
+ * A thenable mutation result that supports chaining mutation modifiers.
137
+ *
138
+ * Each modifier returns a **new** `MutateResult` with the updated state,
139
+ * keeping the original immutable.
140
+ *
141
+ * @typeParam T - The resolved result type.
142
+ */
143
+ var MutateResult = class MutateResult extends ThenableResult {
144
+ /** The current accumulated mutation state. */
145
+ state;
146
+ /** The runner function that executes the mutation with the given state. */
147
+ runner;
148
+ constructor(state, runner) {
149
+ super(() => runner(state), state.safe);
150
+ this.state = state;
151
+ this.runner = runner;
152
+ }
153
+ /**
154
+ * Specifies which columns to return from the mutation (as an array).
155
+ *
156
+ * @param value - Optional column selection map for `.returning()`.
157
+ * @returns A new `MutateResult` with the return selection applied.
158
+ */
159
+ return(value) {
160
+ return new MutateResult({
161
+ ...this.state,
162
+ returnSelect: value,
163
+ hasReturn: true
164
+ }, this.runner);
165
+ }
166
+ /**
167
+ * Specifies which columns to return, resolving to only the **first** row.
168
+ *
169
+ * Behaves like `.return()` but unwraps the array to a single object.
170
+ *
171
+ * @param value - Optional column selection map for `.returning()`.
172
+ * @returns A new `MutateResult` with `returnFirst` enabled.
173
+ */
174
+ returnFirst(value) {
175
+ return new MutateResult({
176
+ ...this.state,
177
+ returnSelect: value,
178
+ returnFirst: true,
179
+ hasReturn: true
180
+ }, this.runner);
181
+ }
182
+ /**
183
+ * Excludes specific fields from the mutation result **after** execution.
184
+ *
185
+ * Unlike `.exclude()` on queries (which affects the SQL projection),
186
+ * `.omit()` removes keys from the result objects in-memory.
187
+ *
188
+ * @param value - A map of `{ fieldName: true }` for fields to remove.
189
+ * @returns A new `MutateResult` with the omit map applied.
190
+ */
191
+ omit(value) {
192
+ return new MutateResult({
193
+ ...this.state,
194
+ omit: value
195
+ }, this.runner);
196
+ }
197
+ /**
198
+ * Wraps the result in a `{ data, error }` discriminated union.
199
+ *
200
+ * When the mutation succeeds, resolves to `{ data: T, error: undefined }`.
201
+ * When it fails, resolves to `{ data: undefined, error: unknown }`
202
+ * instead of rejecting.
203
+ *
204
+ * @returns A new `MutateResult` with safe error handling enabled.
205
+ */
206
+ safe() {
207
+ return new MutateResult({
208
+ ...this.state,
209
+ safe: true
210
+ }, this.runner);
211
+ }
212
+ };
213
+
214
+ //#endregion
215
+ export { MutateResult, QueryResult };
@@ -0,0 +1,393 @@
1
+ import { DialectHelper } from "./dialect.mjs";
2
+ import { ProjectionBuilder } from "./query/projection.mjs";
3
+ import { WhereCompiler } from "./query/where.mjs";
4
+ import { JoinExecutor } from "./query/joins.mjs";
5
+ import { MutateResult, QueryResult } from "./result.mjs";
6
+ import { ResultTransformer } from "./transform.mjs";
7
+
8
+ //#region src/core/runtime.ts
9
+ /**
10
+ * The runtime implementation behind every model instance.
11
+ *
12
+ * Exposes query methods (`findMany`, `findFirst`), mutation methods
13
+ * (`insert`, `update`, `delete`, `upsert`), and lifecycle helpers
14
+ * (`where`, `extend`, `db`, `include`).
15
+ *
16
+ * Internally delegates to specialised helpers:
17
+ * - {@link WhereCompiler} — compiles where clauses.
18
+ * - {@link ProjectionBuilder} — builds column projections.
19
+ * - {@link JoinExecutor} — executes relation joins.
20
+ * - {@link ResultTransformer} — applies post-query transforms.
21
+ *
22
+ * Each call to `.where()` returns a **new** runtime with the additional
23
+ * filter applied, keeping the original immutable.
24
+ */
25
+ var ModelRuntime = class ModelRuntime {
26
+ /** Static configuration for this model. */
27
+ config;
28
+ /** The current where filter applied via `.where()`. */
29
+ currentWhere;
30
+ whereCompiler;
31
+ projection;
32
+ joinExecutor;
33
+ transformer;
34
+ dialectHelper;
35
+ constructor(config, currentWhere) {
36
+ this.config = config;
37
+ this.currentWhere = currentWhere;
38
+ this.dialectHelper = new DialectHelper(config.dialect);
39
+ this.whereCompiler = new WhereCompiler();
40
+ this.projection = new ProjectionBuilder();
41
+ this.joinExecutor = new JoinExecutor(this.dialectHelper);
42
+ this.transformer = new ResultTransformer();
43
+ }
44
+ /** Model discriminator tag. */
45
+ get $model() {
46
+ return "model";
47
+ }
48
+ /** The name of the table this model is bound to. */
49
+ get $modelName() {
50
+ return this.config.tableName;
51
+ }
52
+ /** The user-defined format function, if any. */
53
+ get $format() {
54
+ return this.config.options.format;
55
+ }
56
+ /** The current where clause, exposed for relation descriptors. */
57
+ get $where() {
58
+ return this.currentWhere;
59
+ }
60
+ /** The table name this model is bound to, exposed for relation descriptors. */
61
+ get $tableName() {
62
+ return this.config.tableName;
63
+ }
64
+ /**
65
+ * Returns a new runtime with an additional where filter.
66
+ *
67
+ * The new filter is AND-ed with any existing model-level where clause
68
+ * at execution time.
69
+ *
70
+ * @param value - A where clause (object, SQL, or model reference).
71
+ * @returns A new {@link ModelRuntime} with the filter applied.
72
+ */
73
+ where(value) {
74
+ return new ModelRuntime(this.config, value);
75
+ }
76
+ /**
77
+ * Returns a relation descriptor carrying the model's where clause
78
+ * and the nested relation includes.
79
+ *
80
+ * Used in `.with()` to filter a relation and load nested relations:
81
+ * ```ts
82
+ * userModel.findMany().with({
83
+ * posts: postModel.where({ ... }).include({ comments: true }),
84
+ * });
85
+ * ```
86
+ *
87
+ * @param value - The nested relation include descriptor.
88
+ * @returns A model relation descriptor consumed by the join executor.
89
+ */
90
+ include(value) {
91
+ return {
92
+ __modelRelation: true,
93
+ whereValue: this.currentWhere,
94
+ tableName: this.config.tableName,
95
+ with: value
96
+ };
97
+ }
98
+ /**
99
+ * Creates a new runtime with merged options.
100
+ *
101
+ * Useful for extending a base model with additional format functions,
102
+ * methods, or default where clauses.
103
+ *
104
+ * @param nextOptions - Partial options to merge.
105
+ * @returns A new {@link ModelRuntime} with the merged configuration.
106
+ */
107
+ extend(nextOptions) {
108
+ return new ModelRuntime({
109
+ ...this.config,
110
+ options: {
111
+ ...this.config.options,
112
+ ...nextOptions,
113
+ methods: {
114
+ ...this.config.options.methods ?? {},
115
+ ...nextOptions.methods ?? {}
116
+ },
117
+ format: nextOptions.format ?? this.config.options.format
118
+ }
119
+ });
120
+ }
121
+ /**
122
+ * Creates a new runtime bound to a different database instance.
123
+ *
124
+ * @param db - The new Drizzle database instance.
125
+ * @returns A new {@link ModelRuntime} using the given database.
126
+ */
127
+ db(db) {
128
+ return new ModelRuntime({
129
+ ...this.config,
130
+ db
131
+ });
132
+ }
133
+ /**
134
+ * Returns a thenable that resolves to an array of matching rows.
135
+ *
136
+ * Supports chaining: `.select()`, `.exclude()` (SQL SELECT),
137
+ * `.with()` (relations), `.raw()`, `.safe()`.
138
+ *
139
+ * @returns A {@link QueryResult} that can be awaited or further chained.
140
+ */
141
+ findMany() {
142
+ const runner = async (qState) => {
143
+ const table = this.getTable();
144
+ const whereSql = this.buildEffectiveWhere(table);
145
+ let result;
146
+ if (qState.with) result = await this.joinExecutor.execute({
147
+ db: this.config.db,
148
+ schema: this.config.schema,
149
+ relations: this.config.relations,
150
+ baseTableName: this.config.tableName,
151
+ baseTable: table,
152
+ whereSql,
153
+ withValue: qState.with,
154
+ select: qState.select,
155
+ exclude: qState.exclude,
156
+ limitOne: false
157
+ });
158
+ else {
159
+ const { selectMap } = this.projection.build(table, qState.select, qState.exclude);
160
+ let query = this.config.db.select(selectMap);
161
+ query = query.from(table);
162
+ if (whereSql) query = query.where(whereSql);
163
+ result = await query;
164
+ }
165
+ return this.applyPostQueryTransforms(result, qState);
166
+ };
167
+ return new QueryResult({}, runner);
168
+ }
169
+ /**
170
+ * Returns a thenable that resolves to the first matching row (or `undefined`).
171
+ *
172
+ * Supports chaining: `.select()`, `.exclude()` (SQL SELECT),
173
+ * `.with()` (relations), `.raw()`, `.safe()`.
174
+ *
175
+ * @returns A {@link QueryResult} that can be awaited or further chained.
176
+ */
177
+ findFirst() {
178
+ const runner = async (qState) => {
179
+ const table = this.getTable();
180
+ const whereSql = this.buildEffectiveWhere(table);
181
+ let result;
182
+ if (qState.with) result = await this.joinExecutor.execute({
183
+ db: this.config.db,
184
+ schema: this.config.schema,
185
+ relations: this.config.relations,
186
+ baseTableName: this.config.tableName,
187
+ baseTable: table,
188
+ whereSql,
189
+ withValue: qState.with,
190
+ select: qState.select,
191
+ exclude: qState.exclude,
192
+ limitOne: true
193
+ });
194
+ else {
195
+ const { selectMap } = this.projection.build(table, qState.select, qState.exclude);
196
+ let query = this.config.db.select(selectMap);
197
+ query = query.from(table);
198
+ if (whereSql) query = query.where(whereSql);
199
+ query = query.limit(1);
200
+ result = (await query)[0];
201
+ }
202
+ return this.applyPostQueryTransforms(result, qState);
203
+ };
204
+ return new QueryResult({}, runner);
205
+ }
206
+ /**
207
+ * Returns a promise that resolves to the number of matching rows.
208
+ *
209
+ * Respects the effective where clause (model-level + `.where()`).
210
+ *
211
+ * @returns A `Promise<number>` with the row count.
212
+ */
213
+ async count() {
214
+ const table = this.getTable();
215
+ const whereSql = this.buildEffectiveWhere(table);
216
+ const db = this.config.db;
217
+ const { count: countFn } = await import("drizzle-orm");
218
+ let query = db.select({ count: countFn() });
219
+ query = query.from(table);
220
+ if (whereSql) query = query.where(whereSql);
221
+ return (await query)[0]?.count ?? 0;
222
+ }
223
+ /**
224
+ * Inserts one or more rows into the table.
225
+ *
226
+ * @param value - The row(s) to insert.
227
+ * @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
228
+ */
229
+ insert(value) {
230
+ const runner = async (mState) => {
231
+ const table = this.getTable();
232
+ const withValues = this.config.db.insert(table).values(mState.value);
233
+ let result = await this.execReturning(withValues, mState);
234
+ if (!(mState.hasReturn || Array.isArray(mState.value)) && Array.isArray(result)) result = result[0];
235
+ return this.applyPostMutateTransforms(result, mState);
236
+ };
237
+ return new MutateResult({
238
+ kind: "insert",
239
+ value
240
+ }, runner);
241
+ }
242
+ /**
243
+ * Updates rows matching the current where clause.
244
+ *
245
+ * @param value - The partial row data to set.
246
+ * @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
247
+ */
248
+ update(value) {
249
+ const runner = async (mState) => {
250
+ const table = this.getTable();
251
+ const whereSql = this.buildEffectiveWhere(table);
252
+ let query = this.config.db.update(table);
253
+ query = query.set(mState.value);
254
+ if (whereSql) query = query.where(whereSql);
255
+ const result = await this.execReturning(query, mState);
256
+ return this.applyPostMutateTransforms(result, mState);
257
+ };
258
+ return new MutateResult({
259
+ kind: "update",
260
+ value
261
+ }, runner);
262
+ }
263
+ /**
264
+ * Deletes rows matching the current where clause.
265
+ *
266
+ * @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
267
+ */
268
+ delete() {
269
+ const runner = async (mState) => {
270
+ const table = this.getTable();
271
+ const whereSql = this.buildEffectiveWhere(table);
272
+ let query = this.config.db.delete(table);
273
+ if (whereSql) query = query.where(whereSql);
274
+ const result = await this.execReturning(query, mState);
275
+ return this.applyPostMutateTransforms(result, mState);
276
+ };
277
+ return new MutateResult({ kind: "delete" }, runner);
278
+ }
279
+ /**
280
+ * Inserts a row, or updates it when a conflict is detected.
281
+ *
282
+ * The `value` must contain `insert`, `update`, and optionally `target`.
283
+ *
284
+ * @param value - The upsert descriptor.
285
+ * @returns A {@link MutateResult} that can be awaited or chained with `.return()`.
286
+ */
287
+ upsert(value) {
288
+ const runner = async (mState) => {
289
+ const table = this.getTable();
290
+ const upsertValue = mState.value;
291
+ const insertValues = upsertValue.insert;
292
+ const updateCfg = upsertValue.update;
293
+ const target = this.normalizeUpsertTarget(table, upsertValue.target);
294
+ let updateSet = updateCfg;
295
+ if (typeof updateCfg === "function") updateSet = updateCfg({
296
+ excluded: (field) => table[field],
297
+ inserted: (field) => table[field]
298
+ });
299
+ let query = this.config.db.insert(table);
300
+ query = query.values(insertValues);
301
+ const queryRecord = query;
302
+ if (typeof queryRecord.onConflictDoUpdate === "function") query = queryRecord.onConflictDoUpdate({
303
+ target,
304
+ set: updateSet
305
+ });
306
+ const result = await this.execReturning(query, mState);
307
+ return this.applyPostMutateTransforms(result, mState);
308
+ };
309
+ return new MutateResult({
310
+ kind: "upsert",
311
+ value
312
+ }, runner);
313
+ }
314
+ /**
315
+ * Attaches user-defined methods from the model options to a target object.
316
+ *
317
+ * Each method is bound to `target` so that `this` inside the method
318
+ * refers to the model-like object.
319
+ *
320
+ * @param target - The object to attach methods to.
321
+ */
322
+ attachMethods(target) {
323
+ const methods = this.config.options.methods;
324
+ if (!methods) return;
325
+ for (const [key, fn] of Object.entries(methods)) if (typeof fn === "function") target[key] = fn.bind(target);
326
+ }
327
+ /**
328
+ * Applies `returnFirst` and `omit` transforms to a mutation result.
329
+ */
330
+ applyPostMutateTransforms(result, mState) {
331
+ let out = result;
332
+ if (mState.returnFirst && Array.isArray(out)) out = out[0];
333
+ if (mState.omit) out = this.transformer.applyExclude(out, mState.omit);
334
+ return out;
335
+ }
336
+ /**
337
+ * Retrieves the Drizzle table object for the configured table name.
338
+ */
339
+ getTable() {
340
+ return this.config.schema[this.config.tableName];
341
+ }
342
+ /**
343
+ * Builds the effective where clause by merging the model-level where
344
+ * (from options) with the call-level where (from `.where()`).
345
+ */
346
+ buildEffectiveWhere(table) {
347
+ return this.whereCompiler.compileEffective(table, this.config.options.where, this.currentWhere);
348
+ }
349
+ /**
350
+ * Applies post-query transforms to the query result.
351
+ *
352
+ * Note: `.select()` and `.exclude()` are handled at the SQL level
353
+ * (via {@link ProjectionBuilder}) and are NOT applied here.
354
+ * Only format transforms are applied post-query.
355
+ */
356
+ applyPostQueryTransforms(result, qState) {
357
+ let out = result;
358
+ if (!qState.raw) out = this.transformer.applyFormat(out, this.config.options.format);
359
+ return out;
360
+ }
361
+ /**
362
+ * Executes the returning clause for a mutation query.
363
+ *
364
+ * Handles the dialect-specific differences:
365
+ * - Standard `.returning()` (PostgreSQL, SQLite).
366
+ * - `.$returningId()` (MySQL, SingleStore, CockroachDB).
367
+ *
368
+ * @param query - The built mutation query.
369
+ * @param mState - The mutation state (may contain `returnSelect`).
370
+ * @returns The mutation result.
371
+ */
372
+ async execReturning(query, mState) {
373
+ const queryRecord = query;
374
+ if (typeof queryRecord.returning === "function") return mState.returnSelect ? await queryRecord.returning(mState.returnSelect) : await queryRecord.returning();
375
+ if (this.dialectHelper.isReturningIdOnly() && typeof queryRecord.$returningId === "function") return await queryRecord.$returningId();
376
+ return await query;
377
+ }
378
+ /**
379
+ * Normalizes the upsert conflict target.
380
+ *
381
+ * Converts string column names to their Drizzle column references,
382
+ * handling both single values and arrays.
383
+ */
384
+ normalizeUpsertTarget(table, target) {
385
+ if (!target) return target;
386
+ if (typeof target === "string") return table[target] ?? target;
387
+ if (Array.isArray(target)) return target.map((t) => typeof t === "string" ? table[t] ?? t : t);
388
+ return target;
389
+ }
390
+ };
391
+
392
+ //#endregion
393
+ export { ModelRuntime };
@@ -0,0 +1,83 @@
1
+ //#region src/core/transform.ts
2
+ /**
3
+ * Applies post-query transformations to result sets.
4
+ *
5
+ * Handles three independent transformations that can be composed:
6
+ * - **select** — whitelist specific fields from the result.
7
+ * - **exclude** — blacklist specific fields from the result.
8
+ * - **format** — run a user-defined formatting function over each row.
9
+ *
10
+ * Each method is safe to call on `null`, `undefined`, arrays, and
11
+ * single objects, and recurses correctly into nested structures.
12
+ */
13
+ var ResultTransformer = class {
14
+ /**
15
+ * Picks only the fields present in the `select` map from each row.
16
+ *
17
+ * Supports nested selection: when a select value is an object instead
18
+ * of `true`, it recurses into that nested structure.
19
+ *
20
+ * @param value - The query result (single row, array, or nullish).
21
+ * @param select - A map of `{ fieldName: true | nestedSelect }`.
22
+ * @returns The filtered result with the same shape (single / array).
23
+ */
24
+ applySelect(value, select) {
25
+ if (value == null) return value;
26
+ if (Array.isArray(value)) return value.map((item) => this.applySelect(item, select));
27
+ if (typeof value !== "object") return value;
28
+ const row = value;
29
+ const out = {};
30
+ for (const [key, sel] of Object.entries(select)) {
31
+ if (sel === true) {
32
+ out[key] = row[key];
33
+ continue;
34
+ }
35
+ if (sel && typeof sel === "object") out[key] = this.applySelect(row[key], sel);
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * Removes the fields present in the `exclude` map from each row.
41
+ *
42
+ * Supports nested exclusion: when an exclude value is an object,
43
+ * it recurses into the nested value instead of removing the key entirely.
44
+ *
45
+ * @param value - The query result (single row, array, or nullish).
46
+ * @param exclude - A map of `{ fieldName: true | nestedExclude }`.
47
+ * @returns The result with excluded fields removed.
48
+ */
49
+ applyExclude(value, exclude) {
50
+ if (value == null) return value;
51
+ if (Array.isArray(value)) return value.map((item) => this.applyExclude(item, exclude));
52
+ if (typeof value !== "object") return value;
53
+ const out = { ...value };
54
+ for (const [key, ex] of Object.entries(exclude)) {
55
+ if (ex === true) {
56
+ delete out[key];
57
+ continue;
58
+ }
59
+ if (ex && typeof ex === "object" && key in out) out[key] = this.applyExclude(out[key], ex);
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * Applies a user-defined format function to each row in the result.
65
+ *
66
+ * When the format function is `undefined` or `null`, the value is
67
+ * returned unchanged. Arrays are mapped element-by-element.
68
+ *
69
+ * @param value - The query result (single row, array, or nullish).
70
+ * @param format - A function that transforms a single row, or `undefined`.
71
+ * @returns The formatted result.
72
+ */
73
+ applyFormat(value, format) {
74
+ if (!format) return value;
75
+ if (value == null) return value;
76
+ if (Array.isArray(value)) return value.map((item) => this.applyFormat(item, format));
77
+ if (typeof value !== "object") return value;
78
+ return format(value);
79
+ }
80
+ };
81
+
82
+ //#endregion
83
+ export { ResultTransformer };