@prismakit/cli 2.2.3 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ # PrismaKit core reference
2
+
3
+ API surface for `@prismakit/core` 3.x. Read [SKILL.md](SKILL.md) first.
4
+
5
+ ## Packages
6
+
7
+ | Package | Role |
8
+ |---------|------|
9
+ | `@prismakit/core` | `createRepository`, AutoComposer, locks, pagination, `CacheAdapter` |
10
+ | `@prismakit/redis` | `RedisCacheAdapter` |
11
+ | `@prismakit/memory` | `MemoryCacheAdapter` (tests / local) |
12
+ | `@prismakit/cli` | `prismakit generate / validate / skills` |
13
+ | `@prismakit/eslint-plugin` | Repository-only data-access rules |
14
+
15
+ Node ≥ 20. Install:
16
+
17
+ ```bash
18
+ pnpm add @prismakit/core
19
+ pnpm add @prismakit/redis ioredis # optional production cache
20
+ pnpm add -D @prismakit/eslint-plugin @prismakit/cli
21
+ # tests: pnpm add -D @prismakit/memory
22
+ ```
23
+
24
+ ## Factory
25
+
26
+ ```typescript
27
+ createRepository(options) → new RepoClass(deps: RepositoryDeps)
28
+ ```
29
+
30
+ Aliases: `defineRepository`, `createPrismaRepository`.
31
+
32
+ `RepositoryDeps`: `{ prisma, cache?, registry?, autoCompose? }`.
33
+
34
+ ### `RepositoryOptions`
35
+
36
+ | Field | Type | Notes |
37
+ |-------|------|-------|
38
+ | `model` | `string` | Client key. Required for cache + compose. |
39
+ | `scalarFields` | `Record<string, string>` | `Prisma.XScalarFieldEnum`; inferred from meta if omitted. |
40
+ | `cache` | `CacheOptions \| true` | `true` → `{ ttl: 86400, sensitiveFields: ['password'] }`. |
41
+ | `lock` | `true \| string \| RepositoryLockConfig` | `true` / client key / Pascal name / `@@map` table. |
42
+ | `schemaPath` | `string` | Lock/schema helpers when global meta is missing. |
43
+ | `primaryKey` | `string \| string[]` | `*ById` + row locks. Default: meta PK or `id`. |
44
+ | `getDelegate` | `(client) => delegate` | Default `(c) => c[model]`. |
45
+ | `toPayload` | `(data) => payload` | Default identity. Prefer typed factories over this. |
46
+
47
+ `RepositoryLockConfig`: `{ tableName: string; columns?: Record<string, string> }`.
48
+
49
+ ## Repository methods
50
+
51
+ All methods accept optional `tx`. Cached repos also accept cache fields (see below). `id` is `string | Record<string, string>`.
52
+
53
+ ### Reads
54
+
55
+ | Method | Extra args | Returns |
56
+ |--------|------------|---------|
57
+ | `getById` | `id`, `select?`, `lock?`, `setCache?` | `T \| null` |
58
+ | `getThrowById` | same | `T` (throws if missing) |
59
+ | `getFirst` | `where?`, `select?`, `lock?`, `setCache?`, `cacheTags?` | `T \| null` |
60
+ | `getMany` | `where?`, `select?`, `orderBy?`, `take?`, `skip?`, `lock?`, `setCache?`, `cacheTags?` | `T[]` |
61
+ | `getManyPaginate` | `where?`, `select?`, `orderBy?`, `page?`, `pageSize?`, `setCache?`, `cacheTags?` | `PaginatedResult<T>` |
62
+
63
+ `PaginatedResult<T>`:
64
+
65
+ ```typescript
66
+ { data: T[]; meta: { page: number; pageSize: number; totalItems: number; totalPages: number } }
67
+ ```
68
+
69
+ `cacheTags` may be `string[]` or `(where?) => string[]`.
70
+
71
+ ### Writes
72
+
73
+ | Method | Extra args | Default `invalidate` | Returns |
74
+ |--------|------------|----------------------|---------|
75
+ | `create` | `data`, `select?`, `invalidate?`, `tags?` | `queries` | `T` |
76
+ | `createMany` | `data[]`, `skipDuplicates?`, `invalidate?`, `tags?` | `queries` | `{ count }` |
77
+ | `updateById` | `id`, `data`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
78
+ | `updateMany` | `where`, `data`, `invalidate?`, `tags?` | `all` | `{ count }` |
79
+ | `upsert` | `where`, `create`, `update`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
80
+ | `deleteById` | `id`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
81
+ | `deleteMany` | `where`, `invalidate?`, `tags?` | `all` | `{ count }` |
82
+
83
+ Mutation `tags`: `string[] | null | undefined | ((result) => string[] | null | undefined)`.
84
+
85
+ ### Manual invalidation (cached repos only)
86
+
87
+ ```typescript
88
+ await repo.invalidateCache({ id?: string; tags?: string[] });
89
+ ```
90
+
91
+ ## Cache
92
+
93
+ ### When a read hits cache
94
+
95
+ 1. `setCache: true` **or** `cache.defaultSetCache: true` without `setCache: false`
96
+ 2. `model` + `cache` config present
97
+ 3. No `tx`
98
+ 4. Method not `{ enabled: false }` in `cache.methods`
99
+ 5. `select` has no `sensitiveFields` (default `['password']`)
100
+
101
+ ### `CacheOptions`
102
+
103
+ | Field | Default | Notes |
104
+ |-------|---------|-------|
105
+ | `ttl` | `86400` | Entity TTL (seconds). |
106
+ | `nullTtl` | — | Negative cache for null results. |
107
+ | `sensitiveFields` | `['password']` | Selects containing these never cache. |
108
+ | `methods` | — | Per-method `{ enabled?, ttl? }` for `getById`, `getThrowById`, `getFirst`, `getMany`, `getManyPaginate`. |
109
+ | `defaultSetCache` | `false` | Reads cache unless caller passes `setCache: false`. |
110
+ | `stampede` | see below | Per-repo stampede overrides. |
111
+ | `compression` | — | Hint for adapters (`'none' \| 'zstd' \| 'lz4'`). Redis adapter uses `'none' \| 'gzip'`. |
112
+
113
+ ### `InvalidateMode`
114
+
115
+ | Mode | Effect |
116
+ |------|--------|
117
+ | `all` | Entity keys for `id` (or all entities) + query index |
118
+ | `entity` | Entity keys only |
119
+ | `queries` | Query index only |
120
+ | `none` | Skip — **required inside transactions** |
121
+ | `stale` | Core treats like entity+queries (adapter may layer SWR) |
122
+
123
+ ### Allowlist
124
+
125
+ `setRegisteredCacheModels(['user', 'product'])`. Empty/unset = fail-open. When set, a repo with `cache` whose `model` is missing throws at init.
126
+
127
+ ### Key schema
128
+
129
+ ```
130
+ {prefix}:repo:{model}:e:{id}:{method}:{selectHash}
131
+ {prefix}:repo:{model}:q:{method}:{queryHash}
132
+ {prefix}:repo:{model}:e:{id}:__idx
133
+ {prefix}:repo:{model}:e:__idx
134
+ {prefix}:repo:{model}:q:__idx
135
+ {prefix}:repo:{model}:t:{tag}:__idx
136
+ ```
137
+
138
+ ### Debug
139
+
140
+ ```bash
141
+ CACHE_DEBUG=true
142
+ ```
143
+
144
+ Hits/misses/bypasses via `cacheDebugStorage` from `@prismakit/core`.
145
+
146
+ ### Stampede (`StampedeOptions`)
147
+
148
+ | Field | Default |
149
+ |-------|---------|
150
+ | `lockTtl` | `5` (seconds) |
151
+ | `retryMs` | `100` |
152
+ | `maxRetries` | `10` |
153
+ | `backoff` | `'exponential'` (`'fixed'` also valid) |
154
+ | `totalTimeoutMs` | `3000` |
155
+
156
+ In-process `singleflight` is also used. If Redis is down, fail open.
157
+
158
+ ### Adapters
159
+
160
+ **Redis** (`@prismakit/redis`):
161
+
162
+ ```typescript
163
+ new RedisCacheAdapter({
164
+ url: process.env.REDIS_URL, // or host + port
165
+ host: 'localhost', // default when url omitted
166
+ port: 6379,
167
+ prefix: 'myapp', // default 'prismakit'
168
+ compression: 'gzip', // 'none' | 'gzip'
169
+ compressionThresholdBytes: 1024,
170
+ });
171
+ ```
172
+
173
+ **Memory** (`@prismakit/memory`) — tests/local:
174
+
175
+ ```typescript
176
+ new MemoryCacheAdapter({ prefix: 'test', maxSize: 1000, defaultTtl: 300 });
177
+ ```
178
+
179
+ Custom: implement `CacheAdapter` (`get`/`set`/`del`/`setNx`/`setWithIndex`/`invalidateByIndex`/`saddAndExpire`/`smembers`/`isReady`/`getPrefix` + `safe*` wrappers). `get`/`safeGet` **must** return a fresh copy (structuredClone / JSON). Prefer fail-open `safe*` semantics.
180
+
181
+ ## Auto-compose
182
+
183
+ `splitSelect` keeps scalars (+ FK fields from DMMF) for the Prisma query. Relation keys load via the target repository. Target PK is always injected into the nested select.
184
+
185
+ Load meta once at bootstrap:
186
+
187
+ ```typescript
188
+ import { loadPrismaMetaFromDmmf, loadPrismaMetaFromSchema } from '@prismakit/core';
189
+
190
+ loadPrismaMetaFromDmmf(Prisma.dmmf); // Prisma 5/6
191
+ loadPrismaMetaFromSchema('prisma/schema.prisma'); // Prisma 7 (no Prisma.dmmf)
192
+ ```
193
+
194
+ Without meta, AutoComposer cannot resolve renamed relations; pass `schemaPath` or `dmmf`. Relation field → registry key is source-scoped (`Category.products` vs `Tag.products` can target different models).
195
+
196
+ Validate: `npx prismakit validate --auto-register` or `assertSelectComposeValid`.
197
+
198
+ ### `ComposeOptions` (global via `setComposeOptions`)
199
+
200
+ | Field | Default | Notes |
201
+ |-------|---------|-------|
202
+ | `maxDepth` | `10` | Max relation nesting. |
203
+ | `parallel` | `true` | Same-level relations via `Promise.all`. |
204
+ | `setCache` | `true` | Nested fetches pass `setCache: true` unless parent has `tx` / `setCache: false`. |
205
+ | `tx` | — | Per-call only; forwarded to nested `getMany`. |
206
+
207
+ Related repos must be registered on `RepositoryRegistry`.
208
+
209
+ ## Row locks
210
+
211
+ `SELECT … FOR UPDATE` (and related). Repo `lock` config required. `tx` required on the call.
212
+
213
+ ### Per-call `RowLockOptions`
214
+
215
+ | Field | Notes |
216
+ |-------|-------|
217
+ | `mode` | `'update' \| 'noKeyUpdate' \| 'share' \| 'keyShare'`. Default `noKeyUpdate`. |
218
+ | `nowait` | Fail immediately if locked. |
219
+ | `skipLocked` | Skip locked rows. **Cannot** combine with `nowait`. |
220
+
221
+ `lock: true` / `'wallet'` / `'Wallet'` / `'wallets'` resolve via Prisma meta when available, else `buildLockConfigFromSchema`.
222
+
223
+ ## Telemetry
224
+
225
+ ```typescript
226
+ import { setTelemetry } from '@prismakit/core';
227
+
228
+ setTelemetry({
229
+ enabled: true,
230
+ onEvent: (event) => { /* metrics */ },
231
+ });
232
+ ```
233
+
234
+ | Type | When |
235
+ |------|------|
236
+ | `cache.hit` / `cache.miss` / `cache.bypass` / `cache.invalidate` | Cache-aside path |
237
+ | `compose.start` / `compose.complete` | Auto-compose (`queryCount`, `durationMs`) |
238
+ | `lock.acquired` / `lock.waited` / `lock.timeout` | Row locks |
239
+ | `stampede.locked` / `stampede.waited` / `stampede.fallthrough` | Stampede protection |
240
+ | `query.complete` / `query.slow` | Repository method timing |
241
+
242
+ ## CLI
243
+
244
+ ```bash
245
+ npx prismakit generate <name> [--cache] [--full] [--route <path>] [--prisma-import <path>] [--dry-run]
246
+ npx prismakit validate [--schema <path>] [--auto-register] [--no-assert]
247
+ npx prismakit help
248
+ ```
249
+
250
+ Default generate output: `src/modules/<kebab>/repositories/<kebab>.repository.ts`.
251
+
252
+ `--full` emits Nest module + controller + service + select/where types. Register `*Module` in `app.module.ts` afterwards.
253
+
254
+ ## ESLint
255
+
256
+ ```js
257
+ import prismakit from '@prismakit/eslint-plugin';
258
+ export default [prismakit.configs.recommended];
259
+ ```
260
+
261
+ `recommended` turns all rules on at **error**.
262
+
263
+ | Rule | Forbids |
264
+ |------|---------|
265
+ | `prismakit/no-prisma-service-outside-repos` | Inject/reference `PrismaService` / `PrismaClient` outside allowlist |
266
+ | `prismakit/no-direct-prisma-delegate` | `prisma.<model>.*` outside allowlist |
267
+ | `prismakit/require-transaction-service` | `.$transaction` in feature code |
268
+
269
+ Allowed path patterns (forward slashes):
270
+
271
+ - `**/repositories/**`
272
+ - `**/infrastructure/prisma/**`
273
+ - `**/node_modules/**`
274
+ - `packages/(core|nestjs|redis)/**` (PrismaKit monorepo)
275
+
276
+ Mirror this layout rather than weakening the plugin.
277
+
278
+ ## Bootstrap (plain Node)
279
+
280
+ ```typescript
281
+ import { Prisma, PrismaClient } from '@prisma/client';
282
+ import {
283
+ createRepository,
284
+ loadPrismaMetaFromSchema,
285
+ setRegisteredCacheModels,
286
+ setComposeOptions,
287
+ setTelemetry,
288
+ RepositoryRegistry,
289
+ AutoComposer,
290
+ } from '@prismakit/core';
291
+ import { RedisCacheAdapter } from '@prismakit/redis';
292
+
293
+ const prisma = new PrismaClient();
294
+ loadPrismaMetaFromSchema('prisma/schema.prisma');
295
+ setRegisteredCacheModels(['user', 'post']);
296
+ setComposeOptions({ maxDepth: 6, parallel: true, setCache: true });
297
+ setTelemetry({ enabled: true, onEvent: (e) => console.debug('[pk]', e.type) });
298
+
299
+ const cache = new RedisCacheAdapter({ prefix: 'myapp' });
300
+ const registry = new RepositoryRegistry();
301
+ const autoCompose = new AutoComposer(registry);
302
+
303
+ const UserRepo = createRepository({
304
+ model: 'user',
305
+ scalarFields: Prisma.UserScalarFieldEnum,
306
+ cache: { ttl: 86_400 },
307
+ });
308
+ const users = new UserRepo({ prisma, cache, registry, autoCompose });
309
+ registry.register('user', users);
310
+ ```
311
+
312
+ Nest apps should use `PrismaKitModule` instead of wiring this by hand.
@@ -0,0 +1,72 @@
1
+ # PrismaKit review checklist
2
+
3
+ Copy this list and tick every item before marking a data-access change done. Failures are bugs.
4
+
5
+ ```
6
+ Task Progress:
7
+ - [ ] Layering
8
+ - [ ] Repository factory
9
+ - [ ] Selects
10
+ - [ ] Cache
11
+ - [ ] Transactions
12
+ - [ ] Locks
13
+ - [ ] Compose
14
+ - [ ] ESLint / layout
15
+ ```
16
+
17
+ ## Layering
18
+
19
+ - [ ] No `PrismaClient` / `PrismaService` injected in services, helpers, controllers, processors, or queue workers
20
+ - [ ] No `prisma.<model>.*` outside `**/repositories/**`
21
+ - [ ] Controllers/route handlers call services only — not repositories (unless the app already uses a documented exception)
22
+ - [ ] Helpers inject repositories, never the Prisma client
23
+
24
+ ## Repository factory
25
+
26
+ - [ ] New model access goes through `createRepository` (core) or the app's Nest `defineRepo` binder — not a one-off Prisma call
27
+ - [ ] File lives under `**/repositories/**` as `*.repository.ts`
28
+ - [ ] `model` is the Prisma client key (`'user'`, not `'User'`)
29
+ - [ ] `scalarFields` is set **or** Prisma meta is loaded at bootstrap (`dmmf` / `schemaPath`)
30
+ - [ ] `cacheModels` is omitted unless the app wants an extra allowlist
31
+
32
+ ## Selects
33
+
34
+ - [ ] Every read/write that returns a row passes an explicit `select`
35
+ - [ ] Select presets used: `minimal` (no cache), `general` (API, cacheable), `withPassword` (auth, never cached)
36
+ - [ ] Controllers do not construct `select` objects
37
+ - [ ] Sensitive fields (`password`, tokens) only appear in auth presets
38
+
39
+ ## Cache
40
+
41
+ - [ ] User-facing `getById` / `getThrowById` / `getMany` / `getManyPaginate` pass `setCache: true` when the repo has `cache` (unless `defaultSetCache: true`)
42
+ - [ ] Auth / uniqueness / JWT `getFirst` does **not** pass `setCache: true`
43
+ - [ ] No `setCache` on selects that include `sensitiveFields`
44
+ - [ ] `setCache` / `invalidateCache` not passed on repos that omit `cache` config (types omit those fields)
45
+ - [ ] Query lists that must drop together use `cacheTags` on read and `tags` on write
46
+
47
+ ## Transactions
48
+
49
+ - [ ] Multi-step writes share one transaction; `tx` is passed into **every** repo call in that unit of work
50
+ - [ ] Nest feature code uses `TransactionService.execTx`, not `prisma.$transaction`
51
+ - [ ] Writes inside tx use `invalidate: 'none'`
52
+ - [ ] `invalidateCache` runs after commit (`afterCommit` in Nest, or immediately after `$transaction` in plain Node)
53
+ - [ ] No cached reads mixed with half-committed writes
54
+
55
+ ## Locks
56
+
57
+ - [ ] Repo declares `lock` (`true`, client key, table name, or config) before any call passes `lock`
58
+ - [ ] Every `lock: { mode }` call also passes `tx`
59
+ - [ ] `skipLocked` is not combined with `nowait`
60
+ - [ ] Locked transactions stay short
61
+
62
+ ## Compose
63
+
64
+ - [ ] Relations are nested objects in `select`, not Prisma `include`
65
+ - [ ] Related model repositories are registered (or `autoRegisterModels` covers them)
66
+ - [ ] `npx prismakit validate` (or `validateCompose: true`) is clean when selects nest relations
67
+
68
+ ## ESLint / layout
69
+
70
+ - [ ] `@prismakit/eslint-plugin` `recommended` is in the app ESLint config
71
+ - [ ] Lint passes on the changed files
72
+ - [ ] Prisma client construction stays under `**/infrastructure/prisma/**` (or equivalent allowlisted folder)
@@ -0,0 +1,254 @@
1
+ ---
2
+ name: prismakit-nestjs
3
+ description: >-
4
+ PrismaKit NestJS adapter: PrismaKitModule.forRoot/forRootAsync wiring, injectable repositories via
5
+ createDefineRepo/defineInjectableRepository, TransactionService.execTx with afterCommit invalidation,
6
+ and PRISMAKIT_* DI tokens. Use when working with @prismakit/nestjs, Nest feature modules that need
7
+ Prisma data access, or Nest transactions and cache invalidation.
8
+ ---
9
+
10
+ # PrismaKit NestJS
11
+
12
+ Adapter for NestJS. The data-access contract (layering, cache, compose, locks, ESLint) lives in skill `prismakit` — follow it. This skill covers module wiring, injectable repos, and `TransactionService`.
13
+
14
+ ## Bootstrap
15
+
16
+ ```bash
17
+ pnpm add @prismakit/core @prismakit/nestjs
18
+ pnpm add @prismakit/redis ioredis
19
+ pnpm add -D @prismakit/eslint-plugin @prismakit/cli
20
+ ```
21
+
22
+ ```typescript
23
+ import { Module } from '@nestjs/common';
24
+ import { PrismaKitModule } from '@prismakit/nestjs';
25
+ import { RedisCacheAdapter } from '@prismakit/redis';
26
+ import { Prisma } from '@prisma/client';
27
+
28
+ @Module({
29
+ imports: [
30
+ PrismaKitModule.forRoot({
31
+ prisma: prismaClient,
32
+ cache: new RedisCacheAdapter({ prefix: 'myapp' }),
33
+ schemaPath: 'prisma/schema.prisma', // default; Prisma 5/6: dmmf: Prisma.dmmf
34
+ validateCompose: true,
35
+ compose: { maxDepth: 6, parallel: true, setCache: true },
36
+ }),
37
+ ],
38
+ })
39
+ export class AppModule {}
40
+ ```
41
+
42
+ `forRootAsync` when cache/URL come from `ConfigService` — see [examples.md](examples.md).
43
+
44
+ `schemaPath` defaults to `prisma/schema.prisma`. Always load Prisma meta (`dmmf` or `schemaPath`) so auto-compose and `lock: true` resolve FKs/`@@map` from the schema — no relation-alias map.
45
+
46
+ ## Factory (one default)
47
+
48
+ Bind `Prisma.TypeMap` once, then define repos with runtime options only.
49
+
50
+ ```typescript
51
+ // src/infrastructure/prisma/define-repo.ts
52
+ import { createDefineRepo } from '@prismakit/nestjs';
53
+ import type { Prisma } from '@prisma/client';
54
+
55
+ export const defineRepo = createDefineRepo<Prisma.TypeMap>();
56
+ ```
57
+
58
+ ```typescript
59
+ // src/modules/users/repositories/user.repository.ts
60
+ import { Prisma } from '@prisma/client';
61
+ import { defineRepo } from '../../../infrastructure/prisma/define-repo';
62
+
63
+ const DAY = 86_400;
64
+
65
+ export const UserRepository = defineRepo({
66
+ model: 'user',
67
+ scalarFields: Prisma.UserScalarFieldEnum,
68
+ cache: { ttl: DAY, nullTtl: 60, sensitiveFields: ['password'] },
69
+ lock: true,
70
+ });
71
+ export interface UserRepository extends InstanceType<typeof UserRepository> {}
72
+ ```
73
+
74
+ Escape hatches (do not use as the app default):
75
+
76
+ - `defineInjectableRepository` from `@prismakit/nestjs` (package alias `defineRepository`) — phantoms + payload HKT when TypeMap is unavailable.
77
+ - `createInjectableRepository` without a types bag — thin, results are `unknown`. Alias: `createPrismaRepository`.
78
+
79
+ Do not import `defineRepo` from `@prismakit/nestjs` in apps that already bind `createDefineRepo` as `defineRepo`.
80
+
81
+ ## Register and inject
82
+
83
+ ```typescript
84
+ @Module({
85
+ controllers: [UserController],
86
+ providers: [UserService, UserRepository],
87
+ exports: [UserService, UserRepository],
88
+ })
89
+ export class UserModule {}
90
+ ```
91
+
92
+ ```typescript
93
+ @Injectable()
94
+ export class UserService {
95
+ constructor(private readonly users: UserRepository) {}
96
+ }
97
+ ```
98
+
99
+ **Do not** inject `PRISMAKIT_PRISMA`, `PrismaClient`, or `PrismaService` in services, helpers, controllers, or processors. The prisma token exists so injectable repositories can wire `RepositoryDeps`.
100
+
101
+ Rare custom repos (dashboard/health) may inject `PRISMAKIT_PRISMA` **only** if the class lives under `**/repositories/**`.
102
+
103
+ ## Transactions
104
+
105
+ ```typescript
106
+ import { TransactionService } from '@prismakit/nestjs';
107
+
108
+ constructor(
109
+ private readonly tx: TransactionService,
110
+ private readonly orders: OrderRepository,
111
+ private readonly stocks: StockRepository,
112
+ ) {}
113
+
114
+ await this.tx.execTx(
115
+ async (tx) => {
116
+ const order = await this.orders.create({
117
+ tx,
118
+ data: { /* ... */ },
119
+ select: { id: true },
120
+ invalidate: 'none',
121
+ });
122
+ await this.stocks.updateById({
123
+ tx,
124
+ id: input.stockId,
125
+ data: { qty: { decrement: input.qty } },
126
+ invalidate: 'none',
127
+ });
128
+ return order;
129
+ },
130
+ async () => {
131
+ await this.orders.invalidateCache({});
132
+ await this.stocks.invalidateCache({ id: input.stockId });
133
+ },
134
+ );
135
+ ```
136
+
137
+ Rules:
138
+
139
+ - `TransactionService.execTx(fn, afterCommit?, options?)` — never `prisma.$transaction` in feature code.
140
+ - Pass `tx` into every repo call in the unit of work.
141
+ - Writes inside tx: `invalidate: 'none'`.
142
+ - `afterCommit` runs only after `$transaction` resolves.
143
+ - Keep transactions short. Do not mix cached reads with half-committed writes.
144
+
145
+ Optional third argument: `{ maxWait, timeout, isolationLevel }`.
146
+
147
+ ## Row locks
148
+
149
+ Repo must have `lock` config. Call must pass `tx`.
150
+
151
+ ```typescript
152
+ await this.tx.execTx(
153
+ async (tx) => {
154
+ const wallet = await this.wallets.getById({
155
+ tx,
156
+ id,
157
+ select: { id: true, balance: true },
158
+ lock: { mode: 'update' },
159
+ });
160
+ await this.wallets.updateById({
161
+ tx,
162
+ id,
163
+ data: { balance: wallet!.balance - amount },
164
+ invalidate: 'none',
165
+ });
166
+ },
167
+ async () => {
168
+ await this.wallets.invalidateCache({ id });
169
+ },
170
+ );
171
+ ```
172
+
173
+ ## Cache typing DX
174
+
175
+ When the repo options include `cache`, TypeScript exposes `setCache`, `cacheTags`, mutation `invalidate`/`tags`, and `invalidateCache`. Without `cache`, those fields are omitted — do not pass them.
176
+
177
+ Repository `cache` is the source of truth. Omit `cacheModels` (fail-open). Pass an allowlist only if you want a second check.
178
+
179
+ `cache.defaultSetCache: true` makes user-facing reads cache by default; still pass `setCache: false` on auth/uniqueness.
180
+
181
+ ## What the module provides
182
+
183
+ | Token / provider | Who may inject |
184
+ |------------------|----------------|
185
+ | `TransactionService` | Services / helpers |
186
+ | `RepositoryRegistry` | Kit internals / compose |
187
+ | `AutoComposer` | Kit internals / compose |
188
+ | `PRISMAKIT_PRISMA` | **Repositories only** |
189
+ | `PRISMAKIT_CACHE` | Repositories / kit internals |
190
+ | `PRISMAKIT_OPTIONS` | Kit internals |
191
+
192
+ ## Ops
193
+
194
+ | Option | Use |
195
+ |--------|-----|
196
+ | `cacheModels` | Optional extra allowlist (omit — repo `cache` is enough) |
197
+ | `validateCompose: true` | Assert compose-safe selects on boot |
198
+ | `compose` | `{ maxDepth, parallel, setCache }` |
199
+ | `telemetry` | `{ enabled: true, onEvent }` |
200
+ | `queryLog` | `{ slowThreshold, onSlowQuery }` — enables telemetry |
201
+ | `autoRegisterModels` | `true` or `string[]` — stub repos for compose-only models |
202
+
203
+ ## Scaffolding
204
+
205
+ ```bash
206
+ npx prismakit generate product --cache
207
+ npx prismakit generate product --cache --full --route products
208
+ ```
209
+
210
+ Repo-only: add the class to feature `providers`. `--full`: import `*Module` in `AppModule`. Then `npx prismakit validate`.
211
+
212
+ Enable ESLint `prismakit.configs.recommended` (see skill `prismakit`).
213
+
214
+ ## Clean code (Nest)
215
+
216
+ - One `defineRepo` binder under `src/infrastructure/prisma/`. Do not call `createDefineRepo` per feature.
217
+ - Feature modules own their repository providers; do not make every repo global.
218
+ - Controllers stay HTTP-only: map DTO → service method. No repository calls in controllers.
219
+ - Select presets live next to the repository (`minimal` / `general` / `withPassword`).
220
+ - Name TTL constants once; reuse in repo `cache` config.
221
+
222
+ ## Anti-patterns (BAD → GOOD)
223
+
224
+ ```typescript
225
+ // BAD
226
+ constructor(private readonly prisma: PrismaClient) {}
227
+ constructor(@Inject(PRISMAKIT_PRISMA) private readonly prisma: PrismaClient) {}
228
+ await this.prisma.$transaction(async (tx) => { /* ... */ });
229
+ await this.products.updateById({ tx, id, data }); // invalidates inside tx
230
+ await this.users.getFirst({ where: { email }, select: { id: true }, setCache: true });
231
+
232
+ // GOOD
233
+ constructor(
234
+ private readonly tx: TransactionService,
235
+ private readonly products: ProductRepository,
236
+ ) {}
237
+ await this.tx.execTx(
238
+ async (tx) => {
239
+ await this.products.updateById({ tx, id, data, invalidate: 'none' });
240
+ },
241
+ async () => {
242
+ await this.products.invalidateCache({ id });
243
+ },
244
+ );
245
+ await this.users.getFirst({ where: { email }, select: { id: true } });
246
+ ```
247
+
248
+ ## Before you finish
249
+
250
+ Copy and complete [review-checklist.md](review-checklist.md).
251
+
252
+ - Module options and tokens: [reference.md](reference.md)
253
+ - Production wiring snippets: [examples.md](examples.md)
254
+ - Core contract: skill `prismakit`