vsrepo 2.0.0 → 2.2.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.
Files changed (34) hide show
  1. package/README.md +180 -16
  2. package/README.pt-BR.md +180 -16
  3. package/dist/VSRepoAdapter.d.ts +38 -0
  4. package/dist/VSRepository.d.ts +45 -5
  5. package/dist/VSRepository.js +77 -11
  6. package/dist/decorators/query-method.decorator.d.ts +26 -3
  7. package/dist/decorators/query-method.decorator.js +25 -2
  8. package/dist/index.d.ts +8 -1
  9. package/dist/index.js +6 -1
  10. package/dist/internal/enums/vsrepo-error-type.enum.d.ts +1 -1
  11. package/dist/internal/enums/vsrepo-error-type.enum.js +1 -1
  12. package/dist/internal/resolvers/dynamic-methods.resolver.js +32 -6
  13. package/dist/internal/utils/db-arg.util.d.ts +15 -0
  14. package/dist/internal/utils/db-arg.util.js +22 -0
  15. package/dist/internal/utils/with-db.util.d.ts +19 -0
  16. package/dist/internal/utils/with-db.util.js +23 -0
  17. package/dist/internal/validators/decorators.validator.js +2 -0
  18. package/dist/internal/validators/vsrepo.validator.d.ts +8 -1
  19. package/dist/internal/validators/vsrepo.validator.js +23 -0
  20. package/dist/types/decorators/query-method-options.type.d.ts +35 -1
  21. package/dist/types/utils/decimal-like.type.d.ts +23 -0
  22. package/dist/types/utils/decimal-like.type.js +2 -0
  23. package/dist/types/utils/numeric-keys.type.d.ts +24 -0
  24. package/dist/types/utils/numeric-keys.type.js +2 -0
  25. package/dist/types/utils/numeric-like.type.d.ts +10 -0
  26. package/dist/types/utils/numeric-like.type.js +2 -0
  27. package/dist/types/utils/primitive.type.d.ts +2 -1
  28. package/dist/types/utils/query-args.type.d.ts +30 -0
  29. package/dist/types/utils/query-args.type.js +2 -0
  30. package/dist/types/utils/query-method-arg.type.d.ts +3 -2
  31. package/dist/types/utils/restrict-method-options.type.d.ts +14 -0
  32. package/dist/types/utils/restrict-method-options.type.js +2 -0
  33. package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +14 -0
  34. package/package.json +1 -1
@@ -102,10 +102,12 @@ class VSRepository {
102
102
  wherePkIn(pks) {
103
103
  return { [this.pkName]: { in: pks } };
104
104
  }
105
- async execBaseMethod(fn, methodName, optionsUnchecked) {
106
- const optionsChecked = methodName === "getAll"
107
- ? this.validator.validateGetAllMethodOptions(optionsUnchecked)
108
- : this.validator.validateMethodOptions(optionsUnchecked);
105
+ async execBaseMethod(fn, methodName, optionsUnchecked, opsType = "common") {
106
+ const optionsChecked = opsType === "common"
107
+ ? this.validator.validateMethodOptions(optionsUnchecked)
108
+ : opsType === "restrict"
109
+ ? this.validator.validateRestrictMethodOptions(optionsUnchecked)
110
+ : this.validator.validateGetAllMethodOptions(optionsUnchecked);
109
111
  optionsChecked.db ??= this.getDbClient();
110
112
  const start = this.logger.startPerformLog("run " + methodName);
111
113
  try {
@@ -139,6 +141,8 @@ class VSRepository {
139
141
  * Use `$1`, `$2`, ... placeholders for values passed via `options.args` —
140
142
  * never interpolate values directly into `query`, to avoid SQL injection.
141
143
  * Set `options.modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements.
144
+ * Set `options.singleResult: true` to collapse an array result into its
145
+ * first element (`null` if empty) — see {@link VSRepoQueryOptions.singleResult}.
142
146
  *
143
147
  * @example
144
148
  * ```typescript
@@ -151,6 +155,13 @@ class VSRepository {
151
155
  * 'UPDATE "user" SET active = true WHERE id = $1',
152
156
  * { args: ["123"], modifying: true },
153
157
  * );
158
+ *
159
+ * // Only one row is ever expected here, so `singleResult` collapses the
160
+ * // array into a single object (or `null` when no row matches).
161
+ * const user = await userRepository.query<User | null>(
162
+ * 'SELECT * FROM "user" WHERE id = $1 LIMIT 1',
163
+ * { args: ["123"], singleResult: true },
164
+ * );
154
165
  * ```
155
166
  */
156
167
  async query(query, options) {
@@ -166,8 +177,11 @@ class VSRepository {
166
177
  db: optionsValidated.db,
167
178
  modifying: optionsValidated.modifying ?? false,
168
179
  });
180
+ const resolved = optionsValidated.singleResult && Array.isArray(result)
181
+ ? (result[0] ?? null)
182
+ : result;
169
183
  this.logger.endPerformLog(start);
170
- return result;
184
+ return resolved;
171
185
  }
172
186
  catch (err) {
173
187
  this.logger.endPerformLog(start);
@@ -195,7 +209,7 @@ class VSRepository {
195
209
  return this.execBaseMethod((opt) => this.adapter.findMany(this.mergeWheresResolver.resolve(opt.see, {}), {
196
210
  ...opt,
197
211
  order: opt.order ?? this.defaultOrdering,
198
- }), "getAll", options);
212
+ }), "getAll", options, "getAll");
199
213
  }
200
214
  /** Creates or updates (upsert) a record. */
201
215
  async save(obj, options) {
@@ -217,7 +231,7 @@ class VSRepository {
217
231
  if (!Array.isArray(pks)) {
218
232
  this.fail("'pks' must be a valid array", vsrepo_error_type_enum_1.VSRepoErrorType.BASE);
219
233
  }
220
- return this.execBaseMethod(opt => this.adapter.deleteMany(this.mergeWheresResolver.resolve(opt.see, this.wherePkIn(pks)), opt), "removeList", options);
234
+ return this.execBaseMethod(opt => this.adapter.deleteMany(this.mergeWheresResolver.resolve(opt.see, this.wherePkIn(pks)), opt), "removeList", options, "restrict");
221
235
  }
222
236
  /** Partially updates an existing record by its primary key (PK). */
223
237
  async patch(pk, obj, options) {
@@ -233,11 +247,11 @@ class VSRepository {
233
247
  }
234
248
  /** Returns the total number of records. */
235
249
  async total(options) {
236
- return this.execBaseMethod(opt => this.adapter.count(this.mergeWheresResolver.resolve(opt.see, {}), opt), "total", options);
250
+ return this.execBaseMethod(opt => this.adapter.count(this.mergeWheresResolver.resolve(opt.see, {}), opt), "total", options, "restrict");
237
251
  }
238
252
  /** Checks whether a record exists by its primary key (PK). */
239
253
  async has(pk, options) {
240
- return this.execBaseMethod(opt => this.adapter.exists(this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "has", options);
254
+ return this.execBaseMethod(opt => this.adapter.exists(this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "has", options, "restrict");
241
255
  }
242
256
  /** Marks a record as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
243
257
  async softRemove(pk, options) {
@@ -256,7 +270,7 @@ class VSRepository {
256
270
  this.fail("'pks' must be a valid array", vsrepo_error_type_enum_1.VSRepoErrorType.BASE);
257
271
  }
258
272
  const key = this.softRemoveKey;
259
- return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: new Date() }, opt), "softRemoveList", options);
273
+ return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: new Date() }, opt), "softRemoveList", options, "restrict");
260
274
  }
261
275
  /** Restores a record previously marked as deleted (soft-delete). Requires `softRemoveKey` to be configured on the repository. */
262
276
  async restore(pk, options) {
@@ -275,7 +289,59 @@ class VSRepository {
275
289
  this.fail("'pks' must be a valid array", vsrepo_error_type_enum_1.VSRepoErrorType.BASE);
276
290
  }
277
291
  const key = this.softRemoveKey;
278
- return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: null }, opt), "restoreList", options);
292
+ return this.execBaseMethod(opt => this.adapter.updateMany(this.mergeWheresResolver.resolve(opt.see ?? "all", this.wherePkIn(pks)), { [key]: null }, opt), "restoreList", options, "restrict");
293
+ }
294
+ /**
295
+ * Atomically adds `value` to a numeric field of the record identified
296
+ * by `pk`, evaluated server-side against the row's current value (e.g.
297
+ * `saldo = saldo + value`) — not a fetch-then-save round trip.
298
+ */
299
+ async increment(pk, field, value, options) {
300
+ this.validator.assertIsNumericLike(value);
301
+ return this.execBaseMethod(opt => this.adapter.incrementOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "increment", options);
302
+ }
303
+ /** Same as {@link VSRepository.increment}, subtracting `value` instead of adding it. */
304
+ async decrement(pk, field, value, options) {
305
+ this.validator.assertIsNumericLike(value);
306
+ return this.execBaseMethod(opt => this.adapter.decrementOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "decrement", options);
307
+ }
308
+ /** Same as {@link VSRepository.increment}, multiplying the field's current value by `value`. */
309
+ async multiply(pk, field, value, options) {
310
+ this.validator.assertIsNumericLike(value);
311
+ return this.execBaseMethod(opt => this.adapter.multiplyOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "multiply", options);
312
+ }
313
+ /**
314
+ * Same as {@link VSRepository.increment}, dividing the field's current
315
+ * value by `value`. Division-by-zero behavior depends on the adapter/
316
+ * underlying database (see {@link VSRepoAdapter.divideOne}).
317
+ */
318
+ async divide(pk, field, value, options) {
319
+ this.validator.assertIsNumericLike(value);
320
+ return this.execBaseMethod(opt => this.adapter.divideOne(field, value, this.mergeWheresResolver.resolve(opt.see, this.wherePk(pk)), opt), "divide", options);
321
+ }
322
+ /**
323
+ * Returns the sum of a numeric field across every record matching
324
+ * `where` (all records if omitted), or `null` if none match — mirrors
325
+ * SQL's `SUM()`, which returns `NULL` (not `0`) over an empty set.
326
+ */
327
+ async sum(field, where, options) {
328
+ const validatedWhere = this.validator.validateWhere(where ?? {});
329
+ return this.execBaseMethod(opt => this.adapter.sum(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "sum", options, "restrict");
330
+ }
331
+ /** Same as {@link VSRepository.sum}, but the arithmetic mean instead of the total. */
332
+ async average(field, where, options) {
333
+ const validatedWhere = this.validator.validateWhere(where ?? {});
334
+ return this.execBaseMethod(opt => this.adapter.average(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "average", options, "restrict");
335
+ }
336
+ /** Same as {@link VSRepository.sum}, but the minimum value instead of the total. */
337
+ async min(field, where, options) {
338
+ const validatedWhere = this.validator.validateWhere(where ?? {});
339
+ return this.execBaseMethod(opt => this.adapter.min(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "min", options, "restrict");
340
+ }
341
+ /** Same as {@link VSRepository.sum}, but the maximum value instead of the total. */
342
+ async max(field, where, options) {
343
+ const validatedWhere = this.validator.validateWhere(where ?? {});
344
+ return this.execBaseMethod(opt => this.adapter.max(field, this.mergeWheresResolver.resolve(opt.see, validatedWhere), opt), "max", options, "restrict");
279
345
  }
280
346
  }
281
347
  exports.VSRepository = VSRepository;
@@ -1,15 +1,20 @@
1
- import { QueryMethodOptions } from "../types/decorators/query-method-options.type";
1
+ import type { QueryMethodOptions } from "../types/decorators/query-method-options.type";
2
2
  /**
3
3
  * Property decorator used to declare a raw SQL query method on a `VSRepository`
4
4
  * subclass, bypassing name-based method parsing entirely.
5
5
  *
6
6
  * Applied to a `declare` class field, it executes `value` directly through the
7
7
  * adapter's `query()` method, with parameters injected positionally via the
8
- * `args` array passed at the call site (`$1`, `$2`, ... placeholders).
8
+ * `args` array passed at the call site (`$1`, `$2`, ... placeholders) — or,
9
+ * with `spreadArgs: true`, via separate positional arguments instead.
9
10
  *
10
11
  * @param value Raw SQL statement to execute. Use `$1`, `$2`, ... placeholders for
11
12
  * the values that will be passed via `args` — never interpolate values directly into `value`.
12
- * @param options Optional configuration; set `modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements.
13
+ * @param options Optional configuration; set `modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements,
14
+ * `singleResult: true` to collapse an array result into its first element, and
15
+ * `spreadArgs: true` to receive placeholder values as separate arguments instead of a
16
+ * single `QueryMethodArg` object — see {@link QueryMethodOptions.singleResult} and
17
+ * {@link QueryMethodOptions.spreadArgs}.
13
18
  *
14
19
  * @example
15
20
  * ```typescript
@@ -19,7 +24,25 @@ import { QueryMethodOptions } from "../types/decorators/query-method-options.typ
19
24
  *
20
25
  * @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
21
26
  * declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
27
+ *
28
+ * // Only one row is ever expected here, so `singleResult` collapses the
29
+ * // array into a single object (or `null` when no row matches).
30
+ * @QueryMethod('SELECT * FROM "user" WHERE id = $1 LIMIT 1', { singleResult: true })
31
+ * declare findByIdRaw: (arg: QueryMethodArg<[id: string]>) => Promise<User | null>;
32
+ *
33
+ * // `spreadArgs: true` takes placeholder values as separate arguments,
34
+ * // JpaRepository style, instead of a single `{ args: [...] }` object.
35
+ * // An optional trailing `withDb(tx)` runs the query in a transaction.
36
+ * @QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
37
+ * spreadArgs: true,
38
+ * })
39
+ * declare findByEmailAndType: (
40
+ * ...args: QueryArgs<[email: string, userType: string]>
41
+ * ) => Promise<User[]>;
22
42
  * }
43
+ *
44
+ * await userRepository.findByEmailAndType("joao@email.com", "admin");
45
+ * await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
23
46
  * ```
24
47
  *
25
48
  * @publicApi
@@ -11,11 +11,16 @@ const decorators_validator_1 = require("../internal/validators/decorators.valida
11
11
  *
12
12
  * Applied to a `declare` class field, it executes `value` directly through the
13
13
  * adapter's `query()` method, with parameters injected positionally via the
14
- * `args` array passed at the call site (`$1`, `$2`, ... placeholders).
14
+ * `args` array passed at the call site (`$1`, `$2`, ... placeholders) — or,
15
+ * with `spreadArgs: true`, via separate positional arguments instead.
15
16
  *
16
17
  * @param value Raw SQL statement to execute. Use `$1`, `$2`, ... placeholders for
17
18
  * the values that will be passed via `args` — never interpolate values directly into `value`.
18
- * @param options Optional configuration; set `modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements.
19
+ * @param options Optional configuration; set `modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements,
20
+ * `singleResult: true` to collapse an array result into its first element, and
21
+ * `spreadArgs: true` to receive placeholder values as separate arguments instead of a
22
+ * single `QueryMethodArg` object — see {@link QueryMethodOptions.singleResult} and
23
+ * {@link QueryMethodOptions.spreadArgs}.
19
24
  *
20
25
  * @example
21
26
  * ```typescript
@@ -25,7 +30,25 @@ const decorators_validator_1 = require("../internal/validators/decorators.valida
25
30
  *
26
31
  * @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
27
32
  * declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
33
+ *
34
+ * // Only one row is ever expected here, so `singleResult` collapses the
35
+ * // array into a single object (or `null` when no row matches).
36
+ * @QueryMethod('SELECT * FROM "user" WHERE id = $1 LIMIT 1', { singleResult: true })
37
+ * declare findByIdRaw: (arg: QueryMethodArg<[id: string]>) => Promise<User | null>;
38
+ *
39
+ * // `spreadArgs: true` takes placeholder values as separate arguments,
40
+ * // JpaRepository style, instead of a single `{ args: [...] }` object.
41
+ * // An optional trailing `withDb(tx)` runs the query in a transaction.
42
+ * @QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
43
+ * spreadArgs: true,
44
+ * })
45
+ * declare findByEmailAndType: (
46
+ * ...args: QueryArgs<[email: string, userType: string]>
47
+ * ) => Promise<User[]>;
28
48
  * }
49
+ *
50
+ * await userRepository.findByEmailAndType("joao@email.com", "admin");
51
+ * await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
29
52
  * ```
30
53
  *
31
54
  * @publicApi
package/dist/index.d.ts CHANGED
@@ -3,17 +3,19 @@ export { VSRepository } from "./VSRepository.js";
3
3
  export { VSRepoAdapter } from "./VSRepoAdapter.js";
4
4
  export { VSRepoError } from "./errors/VSRepoError.js";
5
5
  export { VSRepoAdapterError } from "./errors/VSRepoAdapterError.js";
6
+ export { DbArg } from "./internal/utils/db-arg.util.js";
6
7
  export { DynamicMethod } from "./decorators/dynamic-method.decorator.js";
7
8
  export { QueryMethod } from "./decorators/query-method.decorator.js";
8
9
  export { VSRepoErrorType } from "./internal/enums/vsrepo-error-type.enum.js";
9
10
  export { VSLogLevel } from "./internal/enums/vs-log-level.enum.js";
10
11
  export { TransactionIsolationLevel } from "./internal/enums/transaction-isolation-level.enum.js";
11
12
  export { AdapterErrorCode } from "./internal/enums/adapter-error-code.enum.js";
13
+ export { withDb } from "./internal/utils/with-db.util.js";
12
14
  export type { VSRepoOptions } from "./types/vsrepo/vsrepo-options.type.js";
13
15
  export type { VSRepoOrmTypes } from "./types/vsrepo/vsrepo-orm-types.type.js";
14
16
  export type { VSRepoArgs } from "./types/vsrepo/vsrepo-args.type.js";
15
17
  export type { VSRepoMethod } from "./types/vsrepo/vsrepo-method.type.js";
16
- export type { VSRepoWhere, VSRepoWherePlain, VSRepoFieldWhere, VSRepoFieldOperators } from "./types/vsrepo/vsrepo-where.type.js";
18
+ export type { VSRepoWhere, VSRepoWherePlain, VSRepoFieldWhere, VSRepoFieldOperators, } from "./types/vsrepo/vsrepo-where.type.js";
17
19
  export type { VSRepoRelations, RelationKeys } from "./types/vsrepo/vsrepo-relations.type.js";
18
20
  export type { VSRepoSelect } from "./types/vsrepo/vsrepo-select.type.js";
19
21
  export type { VSRepoTransactionOptions } from "./types/vsrepo/vsrepo-transaction-options.type.js";
@@ -31,4 +33,9 @@ export type { Pagination } from "./types/utils/pagination.type.js";
31
33
  export type { Primitive } from "./types/utils/primitive.type.js";
32
34
  export type { SeeMode } from "./types/utils/see-mode.type.js";
33
35
  export type { VSRepoQueryOptions } from "./types/vsrepo/vsrepo-query-options.type";
36
+ export type { DecimalLike } from "./types/utils/decimal-like.type.js";
37
+ export type { NumericKeys } from "./types/utils/numeric-keys.type.js";
38
+ export type { NumericLike } from "./types/utils/numeric-like.type.js";
39
+ export type { RestrictMethodOptions } from "./types/utils/restrict-method-options.type.js";
40
+ export type { QueryArgs } from "./types/utils/query-args.type.js";
34
41
  export { VSLogger } from "./internal/utils/vs-logger.util.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VSLogger = exports.AdapterErrorCode = exports.TransactionIsolationLevel = exports.VSLogLevel = exports.VSRepoErrorType = exports.QueryMethod = exports.DynamicMethod = exports.VSRepoAdapterError = exports.VSRepoError = exports.VSRepoAdapter = exports.VSRepository = void 0;
3
+ exports.VSLogger = exports.withDb = exports.AdapterErrorCode = exports.TransactionIsolationLevel = exports.VSLogLevel = exports.VSRepoErrorType = exports.QueryMethod = exports.DynamicMethod = exports.DbArg = exports.VSRepoAdapterError = exports.VSRepoError = exports.VSRepoAdapter = exports.VSRepository = void 0;
4
4
  require("reflect-metadata");
5
5
  // Public classes / constructors
6
6
  var VSRepository_js_1 = require("./VSRepository.js");
@@ -11,6 +11,8 @@ var VSRepoError_js_1 = require("./errors/VSRepoError.js");
11
11
  Object.defineProperty(exports, "VSRepoError", { enumerable: true, get: function () { return VSRepoError_js_1.VSRepoError; } });
12
12
  var VSRepoAdapterError_js_1 = require("./errors/VSRepoAdapterError.js");
13
13
  Object.defineProperty(exports, "VSRepoAdapterError", { enumerable: true, get: function () { return VSRepoAdapterError_js_1.VSRepoAdapterError; } });
14
+ var db_arg_util_js_1 = require("./internal/utils/db-arg.util.js");
15
+ Object.defineProperty(exports, "DbArg", { enumerable: true, get: function () { return db_arg_util_js_1.DbArg; } });
14
16
  // Decorators
15
17
  var dynamic_method_decorator_js_1 = require("./decorators/dynamic-method.decorator.js");
16
18
  Object.defineProperty(exports, "DynamicMethod", { enumerable: true, get: function () { return dynamic_method_decorator_js_1.DynamicMethod; } });
@@ -25,6 +27,9 @@ var transaction_isolation_level_enum_js_1 = require("./internal/enums/transactio
25
27
  Object.defineProperty(exports, "TransactionIsolationLevel", { enumerable: true, get: function () { return transaction_isolation_level_enum_js_1.TransactionIsolationLevel; } });
26
28
  var adapter_error_code_enum_js_1 = require("./internal/enums/adapter-error-code.enum.js");
27
29
  Object.defineProperty(exports, "AdapterErrorCode", { enumerable: true, get: function () { return adapter_error_code_enum_js_1.AdapterErrorCode; } });
30
+ // Public functions
31
+ var with_db_util_js_1 = require("./internal/utils/with-db.util.js");
32
+ Object.defineProperty(exports, "withDb", { enumerable: true, get: function () { return with_db_util_js_1.withDb; } });
28
33
  // Internal features
29
34
  var vs_logger_util_js_1 = require("./internal/utils/vs-logger.util.js");
30
35
  Object.defineProperty(exports, "VSLogger", { enumerable: true, get: function () { return vs_logger_util_js_1.VSLogger; } });
@@ -8,7 +8,7 @@ export declare enum VSRepoErrorType {
8
8
  DECORATOR = "DECORATOR",
9
9
  /** Failure while resolving a dynamic or query method's configuration into a callable method. */
10
10
  RESOLVER = "RESOLVER",
11
- /** Failure while executing a resolved dynamic method at runtime. */
11
+ /** Failure while executing a resolved dynamic/query method at runtime. */
12
12
  DYNAMIC = "DYNAMIC",
13
13
  /** Invalid method options or arguments detected during validation. */
14
14
  VALIDATOR = "VALIDATOR",
@@ -12,7 +12,7 @@ var VSRepoErrorType;
12
12
  VSRepoErrorType["DECORATOR"] = "DECORATOR";
13
13
  /** Failure while resolving a dynamic or query method's configuration into a callable method. */
14
14
  VSRepoErrorType["RESOLVER"] = "RESOLVER";
15
- /** Failure while executing a resolved dynamic method at runtime. */
15
+ /** Failure while executing a resolved dynamic/query method at runtime. */
16
16
  VSRepoErrorType["DYNAMIC"] = "DYNAMIC";
17
17
  /** Invalid method options or arguments detected during validation. */
18
18
  VSRepoErrorType["VALIDATOR"] = "VALIDATOR";
@@ -12,6 +12,7 @@ const uncapitalize_util_1 = require("../utils/uncapitalize.util");
12
12
  const deepmerge_1 = __importDefault(require("deepmerge"));
13
13
  const vsrepo_error_type_enum_1 = require("../enums/vsrepo-error-type.enum");
14
14
  const debug_arg_symbol_constant_1 = require("../constants/debug-arg-symbol.constant");
15
+ const db_arg_util_1 = require("../utils/db-arg.util");
15
16
  class DynamicMethodsResolver {
16
17
  logger;
17
18
  adapter;
@@ -880,19 +881,44 @@ class DynamicMethodsResolver {
880
881
  this.logger.logDebug(`Resolving ${queryMethods.length} query method(s):`, queryMethods);
881
882
  for (const method of queryMethods) {
882
883
  const originalKey = method.propertyKey;
883
- const modifyingQueryMethod = method.modifying;
884
+ const modifyingQueryMethod = method.modifying ?? false;
884
885
  const valueQueryMethod = method.value;
885
- instance[originalKey] = async (arg) => {
886
- const queryArgValidated = this.validator.validateQueryMethodArg(arg);
887
- queryArgValidated.db ??= this.adapter.getDbClient();
886
+ const singleResult = method.singleResult;
887
+ const spreadArgsMode = method.spreadArgs;
888
+ instance[originalKey] = async (...args) => {
889
+ let db;
890
+ let queryArgs;
891
+ if (spreadArgsMode) {
892
+ const dbPos = args.at(-1);
893
+ if (dbPos instanceof db_arg_util_1.DbArg) {
894
+ db = dbPos.getDb();
895
+ queryArgs = args.slice(0, -1);
896
+ }
897
+ else {
898
+ queryArgs = args;
899
+ }
900
+ }
901
+ else {
902
+ if (args.length > 1) {
903
+ const errorMessage = `This query method was declared without spreadArgs = true, use a single QueryMethodArg instead`;
904
+ this.logger.logError(`Cannot run '${String(originalKey)}': ${errorMessage}`);
905
+ throw new VSRepoError_1.VSRepoError(errorMessage, vsrepo_error_type_enum_1.VSRepoErrorType.DYNAMIC);
906
+ }
907
+ const queryArgValidated = this.validator.validateQueryMethodArg(args[0]);
908
+ db = queryArgValidated.db;
909
+ queryArgs = queryArgValidated.args;
910
+ }
911
+ db ??= this.adapter.getDbClient();
888
912
  const start = this.logger.startPerformLog(`run ${String(originalKey)} (Modifying: ${modifyingQueryMethod})`);
889
913
  try {
890
914
  const result = await this.adapter.query(valueQueryMethod, {
891
- ...queryArgValidated,
915
+ db,
916
+ args: queryArgs,
892
917
  modifying: modifyingQueryMethod,
893
918
  });
919
+ const resolved = singleResult && Array.isArray(result) ? (result[0] ?? null) : result;
894
920
  this.logger.endPerformLog(start);
895
- return result;
921
+ return resolved;
896
922
  }
897
923
  catch (err) {
898
924
  this.logger.endPerformLog(start);
@@ -0,0 +1,15 @@
1
+ import { VSRepoOrmTypes } from "../../types/vsrepo/vsrepo-orm-types.type";
2
+ /**
3
+ * Wraps a database client or transaction so it can be recognized, at
4
+ * runtime, as the trailing `db` override in a {@link QueryArgs} spread call —
5
+ * as opposed to a regular positional query argument. Build one with
6
+ * {@link withDb} rather than constructing it directly.
7
+ *
8
+ * @publicApi
9
+ */
10
+ export declare class DbArg<T extends VSRepoOrmTypes = VSRepoOrmTypes> {
11
+ private readonly db;
12
+ constructor(db: T["dbClient"] | T["dbTransaction"]);
13
+ /** Returns the wrapped database client or transaction. */
14
+ getDb(): T["dbClient"] | T["dbTransaction"];
15
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbArg = void 0;
4
+ /**
5
+ * Wraps a database client or transaction so it can be recognized, at
6
+ * runtime, as the trailing `db` override in a {@link QueryArgs} spread call —
7
+ * as opposed to a regular positional query argument. Build one with
8
+ * {@link withDb} rather than constructing it directly.
9
+ *
10
+ * @publicApi
11
+ */
12
+ class DbArg {
13
+ db;
14
+ constructor(db) {
15
+ this.db = db;
16
+ }
17
+ /** Returns the wrapped database client or transaction. */
18
+ getDb() {
19
+ return this.db;
20
+ }
21
+ }
22
+ exports.DbArg = DbArg;
@@ -0,0 +1,19 @@
1
+ import { VSRepoOrmTypes } from "../../types/vsrepo/vsrepo-orm-types.type";
2
+ import { DbArg } from "./db-arg.util";
3
+ /**
4
+ * Wraps a database client or transaction so it can be passed as the
5
+ * trailing argument of a `@QueryMethod` declared with `{ spreadArgs: true }`,
6
+ * running that call against `db` instead of the repository's default
7
+ * client — the same role `{ db }` plays in the single-object
8
+ * `QueryMethodArg` call style.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * await userRepository.transaction(async (tx) => {
13
+ * await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
14
+ * });
15
+ * ```
16
+ *
17
+ * @publicApi
18
+ */
19
+ export declare function withDb<T extends VSRepoOrmTypes = VSRepoOrmTypes>(db: T["dbClient"] | T["dbTransaction"]): DbArg<T>;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withDb = withDb;
4
+ const db_arg_util_1 = require("./db-arg.util");
5
+ /**
6
+ * Wraps a database client or transaction so it can be passed as the
7
+ * trailing argument of a `@QueryMethod` declared with `{ spreadArgs: true }`,
8
+ * running that call against `db` instead of the repository's default
9
+ * client — the same role `{ db }` plays in the single-object
10
+ * `QueryMethodArg` call style.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * await userRepository.transaction(async (tx) => {
15
+ * await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
16
+ * });
17
+ * ```
18
+ *
19
+ * @publicApi
20
+ */
21
+ function withDb(db) {
22
+ return new db_arg_util_1.DbArg(db);
23
+ }
@@ -59,6 +59,8 @@ class DecoratorsValidator {
59
59
  }
60
60
  static queryMethodOptionsSchema = v.object({
61
61
  modifying: v.optional(v.boolean(), false),
62
+ singleResult: v.optional(v.boolean()),
63
+ spreadArgs: v.optional(v.boolean()),
62
64
  });
63
65
  static validateQueryMethodOptions(options) {
64
66
  const parsed = v.safeParse(this.queryMethodOptionsSchema, options);
@@ -8,6 +8,8 @@ import { Ordering } from "../../types/utils/ordering.type";
8
8
  import { VSLogger } from "../utils/vs-logger.util";
9
9
  import { VSRepoWhere } from "../../types/vsrepo/vsrepo-where.type";
10
10
  import { VSRepoQueryOptions } from "../../types/vsrepo/vsrepo-query-options.type";
11
+ import { NumericLike } from "../../types/utils/numeric-like.type";
12
+ import { RestrictMethodOptions } from "../../types/utils/restrict-method-options.type";
11
13
  export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmTypes> {
12
14
  private logger?;
13
15
  setLogger(logger: VSLogger): void;
@@ -16,13 +18,15 @@ export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmT
16
18
  validateConstructorOptions(options: unknown): VSRepoOptions<T, K>;
17
19
  private readonly methodOptionsSchema;
18
20
  validateMethodOptions(options?: unknown): MethodOptions<T, O>;
21
+ private readonly restrictMethodOptionsSchema;
22
+ validateRestrictMethodOptions(options?: unknown): RestrictMethodOptions<T, O>;
19
23
  private readonly getAllMethodOptionsSchema;
20
24
  validateGetAllMethodOptions(options?: unknown): MethodOptions<T, O> & {
21
25
  pagination?: Pagination;
22
26
  order?: Ordering<T>;
23
27
  };
24
28
  private queryArgSchema;
25
- validateQueryMethodArg(arg?: unknown): QueryMethodArg<any>;
29
+ validateQueryMethodArg(arg?: unknown): QueryMethodArg<any[]>;
26
30
  private queryOptionsSchema;
27
31
  validateQueryOptions(options?: unknown): VSRepoQueryOptions;
28
32
  private transactionOptionsSchema;
@@ -30,4 +34,7 @@ export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmT
30
34
  validateOrdering(value: unknown): Ordering<T>;
31
35
  validatePagination(value: unknown): Pagination;
32
36
  validateWhere(value: unknown): VSRepoWhere<T>;
37
+ private decimalLikeSchema;
38
+ private numericLikeSchema;
39
+ assertIsNumericLike(value: unknown): asserts value is NumericLike;
33
40
  }
@@ -89,6 +89,17 @@ class VSRepoValidator {
89
89
  }
90
90
  return parsed.output;
91
91
  }
92
+ restrictMethodOptionsSchema = v.object({
93
+ db: v.optional(v.any()),
94
+ see: v.optional(v.picklist(["active", "removed", "all"])),
95
+ });
96
+ validateRestrictMethodOptions(options) {
97
+ const parsed = v.safeParse(this.restrictMethodOptionsSchema, options ?? {});
98
+ if (!parsed.success) {
99
+ this.failValidation(parsed.issues[0], vsrepo_error_type_enum_1.VSRepoErrorType.VALIDATOR);
100
+ }
101
+ return parsed.output;
102
+ }
92
103
  getAllMethodOptionsSchema = v.object({
93
104
  ...this.methodOptionsSchema.entries,
94
105
  order: v.optional(ordering_schema_1.default),
@@ -116,6 +127,7 @@ class VSRepoValidator {
116
127
  args: v.optional(v.array(v.any())),
117
128
  db: v.optional(v.any()),
118
129
  modifying: v.optional(v.boolean()),
130
+ singleResult: v.optional(v.boolean()),
119
131
  });
120
132
  validateQueryOptions(options) {
121
133
  const parsed = v.safeParse(this.queryOptionsSchema, options ?? {});
@@ -156,5 +168,16 @@ class VSRepoValidator {
156
168
  }
157
169
  return parsed.output;
158
170
  }
171
+ decimalLikeSchema = v.looseObject({
172
+ toNumber: v.function(),
173
+ decimalPlaces: v.function(),
174
+ });
175
+ numericLikeSchema = v.union([v.number(), v.bigint(), this.decimalLikeSchema]);
176
+ assertIsNumericLike(value) {
177
+ const parsed = v.safeParse(this.numericLikeSchema, value);
178
+ if (!parsed.success) {
179
+ this.failValidation(parsed.issues[0], vsrepo_error_type_enum_1.VSRepoErrorType.VALIDATOR);
180
+ }
181
+ }
159
182
  }
160
183
  exports.VSRepoValidator = VSRepoValidator;
@@ -11,5 +11,39 @@ export type QueryMethodOptions = {
11
11
  * to whatever return type is declared on the field.
12
12
  * @default false
13
13
  */
14
- modifying: boolean;
14
+ modifying?: boolean;
15
+ /**
16
+ * When `true`, the array returned by the underlying query is collapsed
17
+ * into its first element (`null` if the array is empty) before
18
+ * being resolved to the caller. Has no effect when the query resolves
19
+ * to something other than an array (e.g. a `modifying` query's
20
+ * affected-row count).
21
+ *
22
+ * Useful for queries you already know return at most one row (e.g. a
23
+ * `SELECT ... LIMIT 1` or a lookup by a unique column), where declaring
24
+ * the return type as an array would be misleading.
25
+ *
26
+ * @default false
27
+ */
28
+ singleResult?: boolean;
29
+ /**
30
+ * When `true`, the decorated method receives its SQL placeholder values
31
+ * as separate positional arguments (`method(a, b, c)`) instead of a
32
+ * single {@link QueryMethodArg} object (`method({ args: [a, b, c] })`).
33
+ * Type the declared field's parameters with {@link QueryArgs} to get
34
+ * autocompletion and arity checking for this call style.
35
+ *
36
+ * To run the query against a specific client or transaction — instead
37
+ * of the repository's default one — pass {@link DbArg} (built via
38
+ * {@link withDb}) as the trailing argument: `method(a, b, withDb(tx))`.
39
+ * It's recognized by `instanceof`, so it never collides with a regular
40
+ * positional argument, even one that happens to be an object.
41
+ *
42
+ * When `false` (the default), calling the method with more than one
43
+ * argument throws, since it expects the single-object `QueryMethodArg`
44
+ * call style instead.
45
+ *
46
+ * @default false
47
+ */
48
+ spreadArgs?: boolean;
15
49
  };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Structural shape of an arbitrary-precision "Decimal" value, as commonly
3
+ * returned by ORMs for `decimal`/`numeric` columns (e.g. Prisma's
4
+ * `Prisma.Decimal`, built on top of `decimal.js`).
5
+ *
6
+ * Matched structurally (duck-typed) instead of importing a concrete class,
7
+ * so the core stays ORM-agnostic — any object exposing both `toNumber()`
8
+ * and `decimalPlaces()` is treated as Decimal-like by {@link NumericLike}
9
+ * and, transitively, by {@link NumericKeys}.
10
+ *
11
+ * Note that several ORMs (e.g. Drizzle, MikroORM, TypeORM) represent
12
+ * `decimal`/`numeric` columns as plain `string` by default, to avoid
13
+ * floating-point precision loss — a `string` value does **not** satisfy
14
+ * `DecimalLike`. Configure the column in a numeric mode (or provide a
15
+ * transformer) on those ORMs if you want the field to be eligible for
16
+ * `increment`/`decrement`/`multiply`/`divide`/`sum`/`average`/`min`/`max`.
17
+ *
18
+ * @publicApi
19
+ */
20
+ export type DecimalLike = {
21
+ toNumber(): number;
22
+ decimalPlaces(): number;
23
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });