vsrepo 1.3.3 β†’ 1.3.5

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
@@ -4,6 +4,8 @@
4
4
  ![NPM License](https://img.shields.io/npm/l/vsrepo?style=flat-square)
5
5
  ![NPM Downloads](https://img.shields.io/npm/dt/vsrepo?style=flat-square)
6
6
 
7
+ πŸ‡ΊπŸ‡Έ You're reading the English version. [πŸ‡§πŸ‡· Ler em portuguΓͺs](./README.pt-BR.md)
8
+
7
9
  Repository pattern library for projects using **Prisma**, with full **TypeScript** support and automatic **type inference**.
8
10
 
9
11
  VSRepository lets you create strongly-typed repositories with:
@@ -33,7 +35,9 @@ VSRepository lets you create strongly-typed repositories with:
33
35
  - [Merge](#merge)
34
36
  - [Configuring the base methods](#configuring-the-base-methods)
35
37
  - [Select Models](#select-models)
38
+ - [Raw select (options.select)](#raw-select-optionsselect)
36
39
  - [Include Models](#include-models)
40
+ - [Raw include (options.include)](#raw-include-optionsinclude)
37
41
  - [Required Where](#required-where)
38
42
  - [Default Ordenation](#default-ordenation)
39
43
  - [`see` option](#see-option)
@@ -46,6 +50,7 @@ VSRepository lets you create strongly-typed repositories with:
46
50
  - [Distinct](#distinct)
47
51
  - [Method configuration](#method-configuration)
48
52
  - [Aggregate and GroupBy](#aggregate-and-groupby)
53
+ - [Query Methods](#query-methods)
49
54
  - [Relations in save](#relations-in-save)
50
55
  - [Transactions](#transactions)
51
56
  - [Extending a repository](#extending-a-repository)
@@ -100,6 +105,8 @@ npx vsrepo generate \
100
105
 
101
106
  ```
102
107
  generated/vsrepo/
108
+ β”œβ”€β”€ DynamicRepository.ts
109
+ β”œβ”€β”€ DynamicRepository.types.d.ts
103
110
  β”œβ”€β”€ VSRepoError.ts
104
111
  β”œβ”€β”€ VSRepoError.types.d.ts
105
112
  β”œβ”€β”€ VSRepository.ts
@@ -181,7 +188,7 @@ await userRepository.remove(user.id);
181
188
 
182
189
  If you prefer an OOP style with decorators instead of the functional `setupVSRepo` approach, VSRepository also provides `DynamicRepository` β€” a class you can extend with `@DynamicMethod()` decorators to define your dynamic methods.
183
190
 
184
- See **[README-DynamicRepo.md](./README-DynamicRepo.md)** for full documentation on the class-based approach, including NestJS integration examples, decorator config, and a comparison with `setupVSRepo`.
191
+ See **[README-DynamicRepo.md](./README-DynamicRepo.md)** (or the [pt-BR version](./README-DynamicRepo.pt-BR.md)) for full documentation on the class-based approach, including NestJS integration examples, decorator config, and a comparison with `setupVSRepo`.
185
192
 
186
193
  ---
187
194
 
@@ -527,6 +534,35 @@ const user = await userRepository.get(id, { selectModel: "minimal" });
527
534
  const fullUser = await userRepository.get(id, { selectModel: false });
528
535
  ```
529
536
 
537
+ ### Raw `select` (`options.select`)
538
+
539
+ Besides `selectModel` (named, pre-configured in `selectModels`), you can pass a raw Prisma `select` directly in the call, without registering it beforehand on the repository.
540
+
541
+ ```ts
542
+ const user = await userRepository.get(id, {
543
+ select: { id: true, name: true },
544
+ });
545
+ ```
546
+
547
+ `options.select` accepts any valid Prisma `select` for the repository's model β€” it's fully typed and offers the same autocomplete/validation as calling `prisma.user.findMany({ select: ... })` directly, and the method's return type is narrowed to exactly the fields selected.
548
+
549
+ **Rules and behavior:**
550
+
551
+ - **Mutually exclusive with `selectModel`, `includeModel` and `include`.** Only one of the four can be provided per call; the types enforce this β€” passing more than one is a compile-time error.
552
+ - **Ad hoc, not reusable.** Unlike `selectModel`, it doesn't need to be declared in `selectModels`. Use it for one-off projections that don't justify a named select model.
553
+ - **No `defaultSelectModel` is applied.** When `select` is provided, the default select (`defaultSelectModel`) is ignored and only the raw `select` is sent to Prisma.
554
+
555
+ ```ts
556
+ // CORRECT βœ… β€” raw select only
557
+ await userRepository.get(id, { select: { id: true, name: true } });
558
+
559
+ // WRONG ❌ β€” combining select with selectModel/includeModel/include is not allowed
560
+ await userRepository.get(id, { selectModel: "public", select: { id: true } });
561
+ await userRepository.get(id, { include: { posts: true }, select: { id: true } });
562
+ ```
563
+
564
+ > **When to use `selectModel` vs. `select`:** prefer `selectModel` for projections reused across multiple calls (defined once in `selectModels`); use `select` for specific, occasional projections that don't need a name.
565
+
530
566
  ---
531
567
 
532
568
  ## Include Models
@@ -572,6 +608,35 @@ await userRepository.get(id, { selectModel: "public" });
572
608
  await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
573
609
  ```
574
610
 
611
+ ### Raw `include` (`options.include`)
612
+
613
+ Besides `includeModel` (named, pre-configured in `includeModels`), you can pass a raw Prisma `include` directly in the call, without registering it beforehand on the repository.
614
+
615
+ ```ts
616
+ const user = await userRepository.get(id, {
617
+ include: { posts: true, profile: true },
618
+ });
619
+ ```
620
+
621
+ `options.include` accepts any valid `include` for the repository's Prisma model β€” it's fully typed and offers the same autocomplete/validation as calling `prisma.user.findMany({ include: ... })` directly.
622
+
623
+ **Rules and behavior:**
624
+
625
+ - **Mutually exclusive with `selectModel` and `includeModel`.** Only one of the three can be provided per call; the types enforce this β€” passing more than one is a compile-time error.
626
+ - **Ad hoc, not reusable.** Unlike `includeModel`, it doesn't need to be declared in `includeModels`. Use it for one-off includes that don't justify a named model.
627
+ - **No `selectModel` default is applied.** As with `includeModel`, when `include` is provided the select (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
628
+
629
+ ```ts
630
+ // CORRECT βœ… β€” raw include only
631
+ await userRepository.get(id, { include: { posts: true } });
632
+
633
+ // WRONG ❌ β€” combining include with selectModel/includeModel is not allowed
634
+ await userRepository.get(id, { selectModel: "public", include: { posts: true } });
635
+ await userRepository.get(id, { includeModel: "withPosts", include: { posts: true } });
636
+ ```
637
+
638
+ > **When to use `includeModel` vs. `include`:** prefer `includeModel` for includes reused across multiple calls (defined once in `includeModels`); use `include` for specific, occasional includes that don't need a name.
639
+
575
640
  ---
576
641
 
577
642
  ## Required Where
@@ -1022,6 +1087,7 @@ Each entry in `methods` accepts the following options:
1022
1087
  | `pushWhere` | `WhereModel<M>` | β€” | Extra `where` added to the query in addition to `requiredWhere`. |
1023
1088
  | `injectOrdenation` | `OrdenationModel<M>` | β€” | Fixed ordering automatically injected into the query. |
1024
1089
  | `injectPagination` | `PaginationModel<M>` | β€” | Fixed pagination automatically injected into the query. |
1090
+ | `query` | `{ value: string; modifying?: boolean }` | β€” | Turns the method into a **Query Method** (raw SQL). Ignores every other option above β€” see [Query Methods](#query-methods). |
1025
1091
 
1026
1092
  ---
1027
1093
 
@@ -1044,6 +1110,77 @@ const userRepository = setupVSRepo<User, "user">()(({
1044
1110
 
1045
1111
  ---
1046
1112
 
1113
+ ### Query Methods
1114
+
1115
+ Query Methods let a method run **raw SQL** directly, completely bypassing the dynamic-method name parser. They're useful for complex queries (heavy joins, CTEs, database-specific functions) that aren't practical to express with the standard prefixes/suffixes.
1116
+
1117
+ Internally, VSRepository executes the SQL through Prisma using `$queryRawUnsafe` (for reads) or `$executeRawUnsafe` (for writes), and the values in the `args` array are passed as **positional parameters** (`$1`, `$2`, ...) β€” the same prepared-statement technique Prisma itself uses. This means the values are never concatenated into the SQL string, which is what actually prevents SQL injection.
1118
+
1119
+ > [!WARNING]
1120
+ > `$1`, `$2`, ... in your SQL must always represent **values** (data parameters), never column names, table names, or dynamic SQL fragments. Identifier names (columns/tables) can't be passed as a positional parameter β€” if your method needs to vary those, build the SQL from a fixed, known set of options in your own code, never from untrusted input.
1121
+
1122
+ ```ts
1123
+ const userRepository = setupVSRepo<User, "user">()({
1124
+ tableName: "user",
1125
+ pkName: "id",
1126
+ methods: {
1127
+ // Read query method (non-modifying)
1128
+ findActiveUsersRaw: {
1129
+ map: true,
1130
+ query: {
1131
+ value: 'SELECT * FROM "user" WHERE active = $1',
1132
+ },
1133
+ },
1134
+
1135
+ // Write query method (modifying: true)
1136
+ deactivateUsersOlderThanRaw: {
1137
+ map: true,
1138
+ query: {
1139
+ value: 'UPDATE "user" SET active = false WHERE "createdAt" < $1',
1140
+ modifying: true,
1141
+ },
1142
+ },
1143
+ },
1144
+ }).build(prisma);
1145
+ ```
1146
+
1147
+ **Calling a Query Method:**
1148
+
1149
+ Every Query Method takes a single argument shaped as `{ args: [...], db? }`:
1150
+
1151
+ ```ts
1152
+ // Non-modifying: returns 'any' by default, but accepts a generic to
1153
+ // infer/assert the return type right at the call site
1154
+ const activeUsers = await userRepository.findActiveUsersRaw<User[]>({
1155
+ args: [true],
1156
+ });
1157
+
1158
+ // Modifying: always returns 'number' (count of affected rows)
1159
+ const affected = await userRepository.deactivateUsersOlderThanRaw({
1160
+ args: [new Date("2024-01-01")],
1161
+ });
1162
+
1163
+ // Participating in a transaction, via 'db'
1164
+ await userRepository.prisma.$transaction(async (tx) => {
1165
+ await userRepository.deactivateUsersOlderThanRaw({
1166
+ args: [new Date("2024-01-01")],
1167
+ db: tx,
1168
+ });
1169
+ });
1170
+ ```
1171
+
1172
+ | Option | Type | Default | Description |
1173
+ | ------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------|
1174
+ | `value` | `string` | β€” | **Required.** Raw SQL to execute. Use `$1`, `$2`, ... for the placeholders of the values in `args`. |
1175
+ | `modifying` | `boolean` | `false` | When `true`, executes via `$executeRawUnsafe` and the method always resolves to `number`. When `false`, executes via `$queryRawUnsafe` and the method resolves to `TReturn` (`any` by default, inferable via a generic at the call site). |
1176
+
1177
+ > [!NOTE]
1178
+ > Unlike the other dynamic methods, Query Methods **completely ignore** `selectModels`, `requiredWhere`, `pushWhere`, `whereType`, `injectOrdenation`, `injectPagination`, and `proxyTo` β€” none of that applies, since there's no name parsing or `where`/`select` assembly by VSRepository. Free-form method names (outside the `findBy`, `updateBy`, etc. patterns) also **don't** require `proxyTo`.
1179
+
1180
+ The same functionality is available in the class-based approach via the `@QueryMethod` decorator β€” see [README-DynamicRepo.md](./README-DynamicRepo.md#the-querymethod-decorator).
1181
+
1182
+ ---
1183
+
1047
1184
  ## Relations in save
1048
1185
 
1049
1186
  Configure relations so that `save` and `patch` manage them automatically (`saveList` and `patchList` also manage relations automatically).
@@ -1262,6 +1399,8 @@ type OptsModel = MethodOptionsModel<typeof userVSRepo>;
1262
1399
  ```
1263
1400
 
1264
1401
  > The second parameter of `MethodOptions` (`IM`) represents the valid keys of `includeModels`. When provided, `selectModel` and `includeModel` become mutually exclusive in the type β€” it's not possible to pass both in the same call.
1402
+ >
1403
+ > `MethodOptions` also accepts two further generic parameters, `RI` and `RS`, for typing raw `include` and raw `select` respectively (both default to `never`, meaning they're not accepted unless explicitly typed): `MethodOptions<"public", "withPosts", "user", IncludeModel<"user">, SelectModel<"user">>`. `MethodOptionsModel`, derived directly from a configured repository, does not expose `RI`/`RS` β€” use `MethodOptions` directly if you need to type raw `include`/`select` options.
1265
1404
 
1266
1405
  ### Configuration types
1267
1406
 
@@ -1438,9 +1577,9 @@ Recommended `tsconfig.json`:
1438
1577
 
1439
1578
  **Select model returns unexpected fields** β€” Check that the select model defines exactly the fields your TypeScript type expects.
1440
1579
 
1441
- **`selectModel` and `includeModel` together in the same call** β€” Not allowed. Choose one or the other: if `includeModel` is provided, the `select` (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
1580
+ **`selectModel`, `includeModel` and `include` together in the same call** β€” Not allowed. Only one of the three can be provided per call: if `includeModel` or `include` is provided, the `select` (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
1442
1581
 
1443
- **`includeModel` doesn't appear as a default repository option** β€” This is expected. Unlike `defaultSelectModel`, there's no `defaultIncludeModel`/`defaultInclude`. An `includeModel` can only be set in the method call, via `options.includeModel`.
1582
+ **`includeModel` doesn't appear as a default repository option** β€” This is expected. Unlike `defaultSelectModel`, there's no `defaultIncludeModel`/`defaultInclude`. An `includeModel` can only be set in the method call, via `options.includeModel`. A raw, ad hoc include can be set via `options.include`, without needing to be registered in `includeModels`.
1444
1583
 
1445
1584
  **`softRemovekName` throws an error at build** β€” The provided field must be of type `DateTime` in the Prisma schema. Types like `Boolean` or `String` are not accepted.
1446
1585