vsrepo 2.1.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.
package/README.md CHANGED
@@ -51,6 +51,7 @@ VSRepository lets you create strongly-typed repositories with:
51
51
  - [Ordering, pagination and distinct](#ordering-pagination-and-distinct)
52
52
  - [Decorator options](#decorator-options)
53
53
  - [Query methods (raw SQL)](#query-methods-raw-sql)
54
+ - [Spread arguments with `spreadArgs`](#spread-arguments-with-spreadargs)
54
55
  - [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query)
55
56
  - [Transactions](#transactions)
56
57
  - [Utility types](#utility-types)
@@ -598,21 +599,54 @@ class UserRepository extends VSRepository<User, string> {
598
599
 
599
600
  @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
600
601
  declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
602
+
603
+ // Only one row is ever expected here, so `singleResult` collapses the
604
+ // array into a single object (or `null` when no row matches).
605
+ @QueryMethod('SELECT * FROM "user" WHERE id = $1 LIMIT 1', { singleResult: true })
606
+ declare findByIdRaw: (arg: QueryMethodArg<[id: string]>) => Promise<User | null>;
601
607
  }
602
608
  ```
603
609
 
604
- | Option | Type | Default | Description |
605
- | ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
606
- | `modifying` | `boolean` | `false` | When `true`, runs as `INSERT`/`UPDATE`/`DELETE` and the method resolves to the number of affected rows. When `false`, runs as a read query and resolves to the declared return type. |
610
+ | Option | Type | Default | Description |
611
+ | -------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
612
+ | `modifying` | `boolean` | `false` | When `true`, runs as `INSERT`/`UPDATE`/`DELETE` and the method resolves to the number of affected rows. When `false`, runs as a read query and resolves to the declared return type. |
613
+ | `singleResult` | `boolean` | `false` | When `true`, collapses an array result into its first element (`null` if empty), so you can declare the return type as a single object instead of an array. Has no effect on non-array results (e.g. a `modifying` query's affected-row count). |
607
614
 
608
615
  Query methods accept `{ args, db? }` at the call site — `db` lets them participate in a `transaction()` block just like base and dynamic methods.
609
616
 
617
+ ### Spread arguments with `spreadArgs`
618
+
619
+ By default, a `@QueryMethod` receives its placeholder values through a single `QueryMethodArg` object (`method({ args: [...] })`). Set `spreadArgs: true` to receive them as separate positional arguments instead, JpaRepository style:
620
+
621
+ ```typescript
622
+ class UserRepository extends VSRepository<User, string> {
623
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
624
+ spreadArgs: true,
625
+ })
626
+ declare findByEmailAndType: (
627
+ ...args: QueryArgs<[email: string, userType: string]>
628
+ ) => Promise<User[]>;
629
+ }
630
+
631
+ const admins = await userRepository.findByEmailAndType("joao@email.com", "admin");
632
+ ```
633
+
634
+ To run the query against a specific client or transaction instead of the repository's default one, pass `withDb(tx)` as the trailing argument — it wraps `tx` in a `DbArg`, which the resolver recognizes with `instanceof`, so it's never confused with a regular positional argument even if that argument happens to be an object:
635
+
636
+ ```typescript
637
+ await userRepository.transaction(async (tx) => {
638
+ await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
639
+ });
640
+ ```
641
+
642
+ `spreadArgs` only affects `@QueryMethod`-declared fields — it's `false` by default, and calling a method declared without it using more than one argument throws, since the single-`QueryMethodArg` call style is expected instead. It has no effect on `query()`, which always accepts `{ args, db? }`.
643
+
610
644
  ### Ad-hoc raw queries with `query()`
611
645
 
612
646
  For one-off raw SQL that doesn't warrant declaring a `@QueryMethod` on the repository class, call `query()` directly — it's available on every `VSRepository` instance and goes through the same adapter's `query()` implementation under the hood:
613
647
 
614
648
  ```typescript
615
- query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
649
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean; singleResult?: boolean }): Promise<T>;
616
650
  ```
617
651
 
618
652
  ```typescript
@@ -624,13 +658,21 @@ const affectedRows = await userRepository.query<number>(
624
658
  'UPDATE "user" SET active = true WHERE id = $1',
625
659
  { args: ["123"], modifying: true },
626
660
  );
661
+
662
+ // Only one row is ever expected here, so `singleResult` collapses the
663
+ // array into a single object (or `null` when no row matches).
664
+ const user = await userRepository.query<User | null>(
665
+ 'SELECT * FROM "user" WHERE id = $1 LIMIT 1',
666
+ { args: ["123"], singleResult: true },
667
+ );
627
668
  ```
628
669
 
629
- | Option | Type | Default | Description |
630
- | ----------- | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
631
- | `args` | `any[]` | `undefined` | Positional parameters injected into `$1`, `$2`, ... placeholders. Never interpolate values directly into the SQL string. |
632
- | `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
633
- | `modifying` | `boolean` | `false` | When `true`, treats the statement as `INSERT`/`UPDATE`/`DELETE`. |
670
+ | Option | Type | Default | Description |
671
+ | -------------- | --------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
672
+ | `args` | `any[]` | `undefined` | Positional parameters injected into `$1`, `$2`, ... placeholders. Never interpolate values directly into the SQL string. |
673
+ | `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
674
+ | `modifying` | `boolean` | `false` | When `true`, treats the statement as `INSERT`/`UPDATE`/`DELETE`. |
675
+ | `singleResult` | `boolean` | `false` | When `true`, collapses an array result into its first element (`null` if empty). Has no effect on non-array results (e.g. a `modifying` query's affected-row count). |
634
676
 
635
677
  Just like base, dynamic and query methods, `query()` accepts `db` in `options` to participate in a `transaction()` block.
636
678
 
@@ -691,6 +733,7 @@ import type {
691
733
  DeepPartial,
692
734
  CountResult,
693
735
  QueryMethodArg,
736
+ QueryArgs,
694
737
  KeysOfType,
695
738
  NumericKeys,
696
739
  NumericLike,
@@ -713,6 +756,7 @@ import type {
713
756
  | `DeepPartial<T>` | Recursively makes every property of `T` optional, including nested objects and array elements. | `save`, `saveList`, `patch`, `merge`, and every write method on `VSRepoAdapter`. |
714
757
  | `CountResult` | `{ count: number }` — the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
715
758
  | `QueryMethodArg<T>` | `{ args?: T, db? }` — positional SQL parameters (`$1`, `$2`, ...) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
759
+ | `QueryArgs<T, O>` | Types the spread parameter list of a `@QueryMethod` declared with `{ spreadArgs: true }`: `T`'s values in order, followed by an optional trailing `DbArg<O>` built via `withDb()`. | [Spread arguments with `spreadArgs`](#spread-arguments-with-spreadargs). |
716
760
  | `KeysOfType<T, K>` | Extracts the keys of `T` whose value type is assignable to `K`. | Constrains `pkName` in [Constructor options](#constructor-options) to fields of the entity matching the configured primary-key type. |
717
761
  | `NumericKeys<T>` | Extracts the keys of `T` whose (non-nullable) value type is assignable to `NumericLike`. Nullable numeric fields (`number \| null`) are included. | Constrains `field` in [Atomic and aggregate methods](#atomic-and-aggregate-methods) (`increment`, `sum`, etc). |
718
762
  | `NumericLike` | `number \| bigint \| DecimalLike`. | [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
@@ -932,7 +976,7 @@ try {
932
976
  | ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
933
977
  | `DECORATOR` | Invalid arguments were passed to `@DynamicMethod` or `@QueryMethod`. |
934
978
  | `RESOLVER` | The library failed to resolve a dynamic/query method's configuration into a callable method (e.g. an unknown method name). |
935
- | `DYNAMIC` | A resolved dynamic method failed at runtime (e.g. missing arguments). |
979
+ | `DYNAMIC` | A resolved dynamic/query method failed at runtime (e.g. missing arguments). |
936
980
  | `VALIDATOR` | Invalid method options or arguments were detected during validation. |
937
981
  | `BASE` | Invalid usage of a base method (`get`, `save`, `remove`, etc). |
938
982
  | `ADAPTER` | A `VSRepoAdapter` failed while talking to the underlying ORM/database — always thrown as `VSRepoAdapterError`. |
package/README.pt-BR.md CHANGED
@@ -51,6 +51,7 @@ O VSRepository permite criar repositories fortemente tipados com:
51
51
  - [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct)
52
52
  - [Options do decorador](#options-do-decorador)
53
53
  - [Query methods (SQL raw)](#query-methods-sql-raw)
54
+ - [Argumentos via spread com `spreadArgs`](#argumentos-via-spread-com-spreadargs)
54
55
  - [Queries raw pontuais com `query()`](#queries-raw-pontuais-com-query)
55
56
  - [Transações](#transações)
56
57
  - [Tipos utilitários](#tipos-utilitários)
@@ -598,21 +599,54 @@ class UserRepository extends VSRepository<User, string> {
598
599
 
599
600
  @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
600
601
  declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
602
+
603
+ // Aqui só se espera uma linha, então `singleResult` transforma o array
604
+ // em um único objeto (ou `null` quando nenhuma linha corresponde).
605
+ @QueryMethod('SELECT * FROM "user" WHERE id = $1 LIMIT 1', { singleResult: true })
606
+ declare findByIdRaw: (arg: QueryMethodArg<[id: string]>) => Promise<User | null>;
601
607
  }
602
608
  ```
603
609
 
604
- | Option | Tipo | Padrão | Descrição |
605
- | ----------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
606
- | `modifying` | `boolean` | `false` | Quando `true`, executa como `INSERT`/`UPDATE`/`DELETE` e o método resolve para o número de linhas afetadas. Quando `false`, executa como query de leitura e resolve para o tipo de retorno declarado. |
610
+ | Option | Tipo | Padrão | Descrição |
611
+ | -------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
612
+ | `modifying` | `boolean` | `false` | Quando `true`, executa como `INSERT`/`UPDATE`/`DELETE` e o método resolve para o número de linhas afetadas. Quando `false`, executa como query de leitura e resolve para o tipo de retorno declarado. |
613
+ | `singleResult` | `boolean` | `false` | Quando `true`, transforma um resultado em array no seu primeiro elemento (`null` se vazio), permitindo declarar o tipo de retorno como um objeto único em vez de array. Não tem efeito em resultados que não são array (ex.: o número de linhas afetadas de uma query `modifying`). |
607
614
 
608
615
  Query methods aceitam `{ args, db? }` na chamada — `db` permite que participem de um bloco `transaction()`, assim como os métodos base e dinâmicos.
609
616
 
617
+ ### Argumentos via spread com `spreadArgs`
618
+
619
+ Por padrão, um `@QueryMethod` recebe seus valores de placeholder através de um único objeto `QueryMethodArg` (`method({ args: [...] })`). Defina `spreadArgs: true` para recebê-los como argumentos posicionais separados, no estilo do JpaRepository:
620
+
621
+ ```typescript
622
+ class UserRepository extends VSRepository<User, string> {
623
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
624
+ spreadArgs: true,
625
+ })
626
+ declare findByEmailAndType: (
627
+ ...args: QueryArgs<[email: string, userType: string]>
628
+ ) => Promise<User[]>;
629
+ }
630
+
631
+ const admins = await userRepository.findByEmailAndType("joao@email.com", "admin");
632
+ ```
633
+
634
+ Para rodar a query com um client ou transação específico em vez do client padrão do repository, passe `withDb(tx)` como argumento final — ele embrulha `tx` em um `DbArg`, que o resolver reconhece via `instanceof`, então nunca é confundido com um argumento posicional comum, mesmo que esse argumento seja um objeto:
635
+
636
+ ```typescript
637
+ await userRepository.transaction(async (tx) => {
638
+ await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
639
+ });
640
+ ```
641
+
642
+ `spreadArgs` afeta apenas campos declarados com `@QueryMethod` — o padrão é `false`, e chamar um método declarado sem essa opção usando mais de um argumento lança erro, já que se espera o estilo de chamada com um único `QueryMethodArg`. Não tem efeito sobre `query()`, que sempre aceita `{ args, db? }`.
643
+
610
644
  ### Queries raw pontuais com `query()`
611
645
 
612
646
  Para SQL raw pontual que não justifica declarar um `@QueryMethod` na classe do repository, chame `query()` diretamente — ele está disponível em toda instância de `VSRepository` e passa pela mesma implementação de `query()` do adapter por baixo dos panos:
613
647
 
614
648
  ```typescript
615
- query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
649
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean; singleResult?: boolean }): Promise<T>;
616
650
  ```
617
651
 
618
652
  ```typescript
@@ -624,13 +658,21 @@ const linhasAfetadas = await userRepository.query<number>(
624
658
  'UPDATE "user" SET active = true WHERE id = $1',
625
659
  { args: ["123"], modifying: true },
626
660
  );
661
+
662
+ // Aqui só se espera uma linha, então `singleResult` transforma o array
663
+ // em um único objeto (ou `null` quando nenhuma linha corresponde).
664
+ const user = await userRepository.query<User | null>(
665
+ 'SELECT * FROM "user" WHERE id = $1 LIMIT 1',
666
+ { args: ["123"], singleResult: true },
667
+ );
627
668
  ```
628
669
 
629
- | Option | Tipo | Padrão | Descrição |
630
- | ----------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
631
- | `args` | `any[]` | `undefined` | Parâmetros posicionais injetados nos placeholders `$1`, `$2`, ... Nunca interpole valores diretamente na string SQL. |
632
- | `db` | `any` | Client padrão do repository | Client ou transação do banco em que essa query deve rodar. |
633
- | `modifying` | `boolean` | `false` | Quando `true`, trata a instrução como `INSERT`/`UPDATE`/`DELETE`. |
670
+ | Option | Tipo | Padrão | Descrição |
671
+ | -------------- | --------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
672
+ | `args` | `any[]` | `undefined` | Parâmetros posicionais injetados nos placeholders `$1`, `$2`, ... Nunca interpole valores diretamente na string SQL. |
673
+ | `db` | `any` | Client padrão do repository | Client ou transação do banco em que essa query deve rodar. |
674
+ | `modifying` | `boolean` | `false` | Quando `true`, trata a instrução como `INSERT`/`UPDATE`/`DELETE`. |
675
+ | `singleResult` | `boolean` | `false` | Quando `true`, transforma um resultado em array no seu primeiro elemento (`null` se vazio). Não tem efeito em resultados que não são array (ex.: o número de linhas afetadas de uma query `modifying`). |
634
676
 
635
677
  Assim como os métodos base, dinâmicos e query, `query()` aceita `db` em `options` para participar de um bloco `transaction()`.
636
678
 
@@ -694,6 +736,7 @@ import type {
694
736
  DeepPartial,
695
737
  CountResult,
696
738
  QueryMethodArg,
739
+ QueryArgs,
697
740
  KeysOfType,
698
741
  NumericKeys,
699
742
  NumericLike,
@@ -716,6 +759,7 @@ import type {
716
759
  | `DeepPartial<T>` | Torna todas as propriedades de `T` opcionais recursivamente, incluindo objetos aninhados e elementos de array. | `save`, `saveList`, `patch`, `merge`, e todo método de escrita do `VSRepoAdapter`. |
717
760
  | `CountResult` | `{ count: number }` — o formato retornado por operações em lote. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
718
761
  | `QueryMethodArg<T>` | `{ args?: T, db? }` — parâmetros posicionais do SQL (`$1`, `$2`, ...) e cliente de transação para o `@QueryMethod`. | [Query methods (SQL raw)](#query-methods-sql-raw). |
762
+ | `QueryArgs<T, O>` | Tipa a lista de parâmetros via spread de um `@QueryMethod` declarado com `{ spreadArgs: true }`: os valores de `T`, em ordem, seguidos de um `DbArg<O>` opcional construído via `withDb()`. | [Argumentos via spread com `spreadArgs`](#argumentos-via-spread-com-spreadargs). |
719
763
  | `KeysOfType<T, K>` | Extrai as chaves de `T` cujo tipo de valor é atribuível a `K`. | Restringe `pkName`, em [Options do construtor](#options-do-construtor), aos campos da entidade compatíveis com o tipo de chave primária configurado. |
720
764
  | `NumericKeys<T>` | Extrai as chaves de `T` cujo tipo de valor (ignorando `null`/`undefined`) é atribuível a `NumericLike`. Campos numéricos nullable (`number \| null`) são incluídos. | Restringe `field` em [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação) (`increment`, `sum`, etc). |
721
765
  | `NumericLike` | `number \| bigint \| DecimalLike`. | [Métodos atômicos e de agregação](#métodos-atômicos-e-de-agregação). |
@@ -935,7 +979,7 @@ try {
935
979
  | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
936
980
  | `DECORATOR` | Argumentos inválidos foram passados para `@DynamicMethod` ou `@QueryMethod`. |
937
981
  | `RESOLVER` | A biblioteca falhou ao resolver a configuração de um método dinâmico/de query em um método chamável (ex.: um nome de método desconhecido). |
938
- | `DYNAMIC` | Um método dinâmico já resolvido falhou em tempo de execução (ex.: argumentos faltando). |
982
+ | `DYNAMIC` | Um dynamic/query method já resolvido falhou em tempo de execução (ex.: argumentos faltando). |
939
983
  | `VALIDATOR` | Options ou argumentos de método inválidos foram detectados durante a validação. |
940
984
  | `BASE` | Uso inválido de um método base (`get`, `save`, `remove`, etc). |
941
985
  | `ADAPTER` | Um `VSRepoAdapter` falhou ao falar com o ORM/banco subjacente — sempre é lançado como `VSRepoAdapterError`. |
@@ -79,6 +79,8 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
79
79
  * Use `$1`, `$2`, ... placeholders for values passed via `options.args` —
80
80
  * never interpolate values directly into `query`, to avoid SQL injection.
81
81
  * Set `options.modifying: true` for `INSERT`/`UPDATE`/`DELETE` statements.
82
+ * Set `options.singleResult: true` to collapse an array result into its
83
+ * first element (`null` if empty) — see {@link VSRepoQueryOptions.singleResult}.
82
84
  *
83
85
  * @example
84
86
  * ```typescript
@@ -91,6 +93,13 @@ export declare abstract class VSRepository<Entity, PKType, OrmTypes extends VSRe
91
93
  * 'UPDATE "user" SET active = true WHERE id = $1',
92
94
  * { args: ["123"], modifying: true },
93
95
  * );
96
+ *
97
+ * // Only one row is ever expected here, so `singleResult` collapses the
98
+ * // array into a single object (or `null` when no row matches).
99
+ * const user = await userRepository.query<User | null>(
100
+ * 'SELECT * FROM "user" WHERE id = $1 LIMIT 1',
101
+ * { args: ["123"], singleResult: true },
102
+ * );
94
103
  * ```
95
104
  */
96
105
  query<T = any>(query: string, options?: VSRepoQueryOptions<OrmTypes>): Promise<T>;
@@ -141,6 +141,8 @@ class VSRepository {
141
141
  * Use `$1`, `$2`, ... placeholders for values passed via `options.args` —
142
142
  * never interpolate values directly into `query`, to avoid SQL injection.
143
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}.
144
146
  *
145
147
  * @example
146
148
  * ```typescript
@@ -153,6 +155,13 @@ class VSRepository {
153
155
  * 'UPDATE "user" SET active = true WHERE id = $1',
154
156
  * { args: ["123"], modifying: true },
155
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
+ * );
156
165
  * ```
157
166
  */
158
167
  async query(query, options) {
@@ -168,8 +177,11 @@ class VSRepository {
168
177
  db: optionsValidated.db,
169
178
  modifying: optionsValidated.modifying ?? false,
170
179
  });
180
+ const resolved = optionsValidated.singleResult && Array.isArray(result)
181
+ ? (result[0] ?? null)
182
+ : result;
171
183
  this.logger.endPerformLog(start);
172
- return result;
184
+ return resolved;
173
185
  }
174
186
  catch (err) {
175
187
  this.logger.endPerformLog(start);
@@ -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,12 +3,14 @@ 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";
@@ -35,4 +37,5 @@ export type { DecimalLike } from "./types/utils/decimal-like.type.js";
35
37
  export type { NumericKeys } from "./types/utils/numeric-keys.type.js";
36
38
  export type { NumericLike } from "./types/utils/numeric-like.type.js";
37
39
  export type { RestrictMethodOptions } from "./types/utils/restrict-method-options.type.js";
40
+ export type { QueryArgs } from "./types/utils/query-args.type.js";
38
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);
@@ -26,7 +26,7 @@ export declare class VSRepoValidator<T, K, O extends VSRepoOrmTypes = VSRepoOrmT
26
26
  order?: Ordering<T>;
27
27
  };
28
28
  private queryArgSchema;
29
- validateQueryMethodArg(arg?: unknown): QueryMethodArg<any>;
29
+ validateQueryMethodArg(arg?: unknown): QueryMethodArg<any[]>;
30
30
  private queryOptionsSchema;
31
31
  validateQueryOptions(options?: unknown): VSRepoQueryOptions;
32
32
  private transactionOptionsSchema;
@@ -127,6 +127,7 @@ class VSRepoValidator {
127
127
  args: v.optional(v.array(v.any())),
128
128
  db: v.optional(v.any()),
129
129
  modifying: v.optional(v.boolean()),
130
+ singleResult: v.optional(v.boolean()),
130
131
  });
131
132
  validateQueryOptions(options) {
132
133
  const parsed = v.safeParse(this.queryOptionsSchema, options ?? {});
@@ -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,30 @@
1
+ import { DbArg } from "../../internal/utils/db-arg.util";
2
+ import { VSRepoOrmTypes } from "../vsrepo/vsrepo-orm-types.type";
3
+ /**
4
+ * Types the parameter list of a `@QueryMethod` declared with
5
+ * `{ spreadArgs: true }`: the SQL placeholder values (`T`), in order,
6
+ * followed by an optional trailing {@link DbArg} — built via {@link withDb} —
7
+ * to run the query against a specific client or transaction instead of the
8
+ * repository's default one.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * class UserRepository extends VSRepository<User, string> {
13
+ * @QueryMethod('SELECT * FROM "user" WHERE email = $1 AND "userType" = $2', {
14
+ * spreadArgs: true,
15
+ * })
16
+ * declare findByEmailAndType: (
17
+ * ...args: QueryArgs<[email: string, userType: string]>
18
+ * ) => Promise<User[]>;
19
+ * }
20
+ *
21
+ * await userRepository.findByEmailAndType("joao@email.com", "admin");
22
+ * await userRepository.findByEmailAndType("joao@email.com", "admin", withDb(tx));
23
+ * ```
24
+ *
25
+ * @publicApi
26
+ */
27
+ export type QueryArgs<T extends Array<any> = [], O extends VSRepoOrmTypes = VSRepoOrmTypes> = [
28
+ ...T,
29
+ db?: DbArg<O>
30
+ ];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,3 +1,4 @@
1
+ import { VSRepoOrmTypes } from "../vsrepo/vsrepo-orm-types.type";
1
2
  /**
2
3
  * Single argument accepted by a method declared with `@QueryMethod`.
3
4
  *
@@ -19,9 +20,9 @@
19
20
  *
20
21
  * @publicApi
21
22
  */
22
- export type QueryMethodArg<T extends Array<any>> = {
23
+ export type QueryMethodArg<T extends Array<any> = [], O extends VSRepoOrmTypes = VSRepoOrmTypes> = {
23
24
  /** Positional parameters injected into the SQL placeholders (`$1`, `$2`, ...). */
24
25
  args?: T;
25
26
  /** Database client or transaction to run this query in, instead of the repository's default client. */
26
- db?: any;
27
+ db?: O["dbClient"] | O["dbTransaction"];
27
28
  };
@@ -14,4 +14,18 @@ export type VSRepoQueryOptions<T extends VSRepoOrmTypes = VSRepoOrmTypes> = {
14
14
  * @default false
15
15
  */
16
16
  modifying?: boolean;
17
+ /**
18
+ * When `true`, the array returned by the underlying query is collapsed
19
+ * into its first element (`null` if the array is empty) before
20
+ * being resolved to the caller. Has no effect when the query resolves
21
+ * to something other than an array (e.g. a `modifying` query's
22
+ * affected-row count).
23
+ *
24
+ * Useful for queries you already know return at most one row (e.g. a
25
+ * `SELECT ... LIMIT 1` or a lookup by a unique column), where declaring
26
+ * the return type as an array would be misleading.
27
+ *
28
+ * @default false
29
+ */
30
+ singleResult?: boolean;
17
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vsrepo",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "ORM-agnostic repository pattern library with full TypeScript support and automatic type inference.",
5
5
  "homepage": "https://github.com/jaobrabo123/VSRepository#readme",
6
6
  "repository": {