@prismakit/cli 2.2.3 → 3.0.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 +11 -5
- package/dist/bin.cjs +181 -63
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +26 -15
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-7E6MWEJP.js → chunk-A2GZ2IEY.js} +159 -55
- package/dist/chunk-A2GZ2IEY.js.map +1 -0
- package/dist/index.cjs +161 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -10
- package/dist/index.d.ts +30 -10
- package/dist/index.js +5 -3
- package/package.json +9 -6
- package/rules/data-access.mdc +25 -0
- package/skills/README.md +40 -0
- package/skills/prismakit/SKILL.md +282 -0
- package/skills/prismakit/examples.md +238 -0
- package/skills/prismakit/reference.md +312 -0
- package/skills/prismakit/review-checklist.md +72 -0
- package/skills/prismakit-nestjs/SKILL.md +255 -0
- package/skills/prismakit-nestjs/examples.md +235 -0
- package/skills/prismakit-nestjs/reference.md +146 -0
- package/skills/prismakit-nestjs/review-checklist.md +57 -0
- package/src/__tests__/skills.spec.ts +117 -0
- package/src/bin.ts +29 -14
- package/src/commands/skills.ts +207 -0
- package/src/commands/validate.ts +8 -2
- package/src/index.ts +6 -1
- package/dist/chunk-7E6MWEJP.js.map +0 -1
- package/src/commands/codegen.ts +0 -69
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prismakit
|
|
3
|
+
description: >-
|
|
4
|
+
PrismaKit data-access contract for Prisma apps: repository-only access via createRepository,
|
|
5
|
+
cache-aside with setCache/invalidate, auto-compose relations, row locks, telemetry, prismakit CLI,
|
|
6
|
+
and @prismakit/eslint-plugin. Use when working with @prismakit/core, files under repositories/,
|
|
7
|
+
CacheAdapter/Redis caching of Prisma reads, or when code injects PrismaClient outside a repository.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# PrismaKit
|
|
11
|
+
|
|
12
|
+
Framework-agnostic Prisma repository kit. Not a Prisma fork. Only repositories talk to Prisma.
|
|
13
|
+
|
|
14
|
+
NestJS apps: also load skill `prismakit-nestjs` after this contract.
|
|
15
|
+
|
|
16
|
+
## Non-negotiables
|
|
17
|
+
|
|
18
|
+
Violations are bugs. Enforce with `@prismakit/eslint-plugin` + this skill.
|
|
19
|
+
|
|
20
|
+
| Forbidden | Why |
|
|
21
|
+
|-----------|-----|
|
|
22
|
+
| `PrismaClient` / `PrismaService` in services, helpers, controllers, processors | Bypasses cache, compose, invalidation |
|
|
23
|
+
| `prisma.<model>.*` outside `**/repositories/**` | Same |
|
|
24
|
+
| `$transaction` in Nest feature code | Use `TransactionService.execTx` |
|
|
25
|
+
| `setCache: true` on auth / uniqueness `getFirst` | Stale nulls / race hazards |
|
|
26
|
+
| Caching selects with `password` (or other `sensitiveFields`) | Security |
|
|
27
|
+
| Row `lock` without `tx` | Lock must live inside a transaction |
|
|
28
|
+
| Prisma `include` for relations | Use relation keys in `select` (auto-compose) |
|
|
29
|
+
|
|
30
|
+
| Required | How |
|
|
31
|
+
|----------|-----|
|
|
32
|
+
| Reads/writes | `*Repository` from `createRepository` / Nest factories |
|
|
33
|
+
| Tx writes | `invalidate: 'none'` then `invalidateCache` after commit |
|
|
34
|
+
| User-facing reads | `setCache: true` when the repo has cache config |
|
|
35
|
+
| Relations in `select` | `model` + `scalarFields` or loaded Prisma meta |
|
|
36
|
+
| ESLint | `prismakit.configs.recommended` |
|
|
37
|
+
|
|
38
|
+
## Layers
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Controller / route handler → Service → Helper → Repository → Prisma / CacheAdapter
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Helpers may inject repositories — never the Prisma client.
|
|
45
|
+
|
|
46
|
+
## Decision table
|
|
47
|
+
|
|
48
|
+
| Task | Use |
|
|
49
|
+
|------|-----|
|
|
50
|
+
| New repository | `createRepository` (core) or Nest `defineRepo` — see `prismakit-nestjs` |
|
|
51
|
+
| User-facing get by id | `getThrowById` / `getById` + `setCache: true` |
|
|
52
|
+
| Existence / uniqueness / auth | `getFirst` — **no** `setCache` |
|
|
53
|
+
| List | `getMany` + `setCache: true` + optional `cacheTags` |
|
|
54
|
+
| Paginated list | `getManyPaginate` |
|
|
55
|
+
| Create / update / delete | matching mutation; default invalidation is enough outside tx |
|
|
56
|
+
| Multi-step write | one transaction; pass `tx` into every repo call |
|
|
57
|
+
| `SELECT … FOR UPDATE` | `lock: { mode: 'update' }` **inside** `tx` |
|
|
58
|
+
| Load relations | nested objects in `select`, not Prisma `include` |
|
|
59
|
+
|
|
60
|
+
## Create a repository (core)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { Prisma, PrismaClient } from '@prisma/client';
|
|
64
|
+
import { createRepository } from '@prismakit/core';
|
|
65
|
+
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
66
|
+
|
|
67
|
+
const DAY = 86_400;
|
|
68
|
+
|
|
69
|
+
const UserRepoClass = createRepository({
|
|
70
|
+
model: 'user',
|
|
71
|
+
scalarFields: Prisma.UserScalarFieldEnum,
|
|
72
|
+
cache: { ttl: DAY, nullTtl: 60, sensitiveFields: ['password'] },
|
|
73
|
+
lock: true, // table + columns from Prisma meta
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const prisma = new PrismaClient();
|
|
77
|
+
const cache = new RedisCacheAdapter({ prefix: 'myapp' });
|
|
78
|
+
export const users = new UserRepoClass({ prisma, cache });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`defineRepository` and `createPrismaRepository` are aliases of `createRepository`.
|
|
82
|
+
|
|
83
|
+
| Option | Description |
|
|
84
|
+
|--------|-------------|
|
|
85
|
+
| `model` | Prisma client key (`prisma.user` → `'user'`). Needed for cache + compose. |
|
|
86
|
+
| `scalarFields` | Usually `Prisma.XScalarFieldEnum`. Enables select-split + compose. Optional if DMMF/schema meta is loaded. |
|
|
87
|
+
| `cache` | `CacheOptions` or `true` → `{ ttl: 86400, sensitiveFields: ['password'] }`. |
|
|
88
|
+
| `lock` | `true` / client key / `@@map` table / `{ tableName, columns }`. |
|
|
89
|
+
| `primaryKey` | `string` or `string[]` (composite). Defaults to meta PK or `id`. |
|
|
90
|
+
| `schemaPath` | Path to `schema.prisma` when meta is not loaded globally. |
|
|
91
|
+
| `getDelegate` | Optional. Defaults to `(c) => c[model]`. |
|
|
92
|
+
|
|
93
|
+
Put files under `**/repositories/**`. Keep Prisma client construction under `**/infrastructure/prisma/**`.
|
|
94
|
+
|
|
95
|
+
## Reads
|
|
96
|
+
|
|
97
|
+
Always pass an explicit `select`. Never rely on “fetch all columns”.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
await users.getThrowById({
|
|
101
|
+
id,
|
|
102
|
+
select: { id: true, email: true, name: true },
|
|
103
|
+
setCache: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await users.getFirst({
|
|
107
|
+
where: { email },
|
|
108
|
+
select: { id: true, password: true },
|
|
109
|
+
// no setCache — auth / uniqueness
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Method | Returns |
|
|
114
|
+
|--------|---------|
|
|
115
|
+
| `getById` | payload or `null` |
|
|
116
|
+
| `getThrowById` | payload; throws if missing |
|
|
117
|
+
| `getFirst` | first match or `null` |
|
|
118
|
+
| `getMany` | array (`take` / `skip` / `orderBy`) |
|
|
119
|
+
| `getManyPaginate` | `{ data, meta: { page, pageSize, totalItems, totalPages } }` |
|
|
120
|
+
|
|
121
|
+
`id` is `string` or `Record<string, string>` for composite PKs.
|
|
122
|
+
|
|
123
|
+
## Writes
|
|
124
|
+
|
|
125
|
+
| Method | Default `invalidate` |
|
|
126
|
+
|--------|----------------------|
|
|
127
|
+
| `create` / `createMany` | `queries` |
|
|
128
|
+
| `updateById` / `updateMany` / `upsert` / `deleteById` / `deleteMany` | `all` |
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
await users.updateById({
|
|
132
|
+
id,
|
|
133
|
+
data: { name: 'Ada' },
|
|
134
|
+
select: { id: true, name: true },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Cache
|
|
139
|
+
|
|
140
|
+
A read uses the cache **only when all of these are true**:
|
|
141
|
+
|
|
142
|
+
1. `setCache: true` (or repo `cache.defaultSetCache: true` and caller did not pass `false`)
|
|
143
|
+
2. Repository has `model` + `cache` config
|
|
144
|
+
3. No `tx` (transactions never cache)
|
|
145
|
+
4. Method is not disabled in `cache.methods`
|
|
146
|
+
5. `select` does not include a sensitive field (default includes `password`)
|
|
147
|
+
|
|
148
|
+
If Redis is down, `RedisCacheAdapter` **fails open** — queries still hit Prisma.
|
|
149
|
+
|
|
150
|
+
| Scenario | `setCache` |
|
|
151
|
+
|----------|------------|
|
|
152
|
+
| API detail / list | `true` |
|
|
153
|
+
| Auth, uniqueness, JWT lookup | omit / `false` |
|
|
154
|
+
| Inside `tx` | ignored |
|
|
155
|
+
|
|
156
|
+
Empty `cacheModels` allowlist is **fail-open**. In production, register allowed model keys; a cached repo whose model is missing from the list throws at init.
|
|
157
|
+
|
|
158
|
+
`setCache` / `cacheTags` / `invalidate` / `invalidateCache` exist on the type **only** when the repo has `cache` config. Do not force them on uncached repos.
|
|
159
|
+
|
|
160
|
+
## Transactions (plain Node)
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
await prisma.$transaction(async (tx) => {
|
|
164
|
+
await users.updateById({ tx, id, data, invalidate: 'none' });
|
|
165
|
+
});
|
|
166
|
+
await users.invalidateCache({ id });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Wrap this in an app helper so call sites stay consistent. Nest: `TransactionService.execTx` (skill `prismakit-nestjs`).
|
|
170
|
+
|
|
171
|
+
Never invalidate cache inside the transaction. If the tx rolls back, cache must still be valid.
|
|
172
|
+
|
|
173
|
+
## Auto-compose
|
|
174
|
+
|
|
175
|
+
Relations in `select` load through other registered repositories — not Prisma `include`.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
await posts.getThrowById({
|
|
179
|
+
id,
|
|
180
|
+
select: {
|
|
181
|
+
id: true,
|
|
182
|
+
title: true,
|
|
183
|
+
author: { select: { id: true, name: true } },
|
|
184
|
+
},
|
|
185
|
+
setCache: true,
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Requirements: source repo has `model`; `scalarFields` **or** Prisma meta loaded (`loadPrismaMetaFromDmmf(Prisma.dmmf)` on Prisma 5/6, `loadPrismaMetaFromSchema('prisma/schema.prisma')` on Prisma 7); related model repos are registered.
|
|
190
|
+
|
|
191
|
+
AutoComposer injects the target primary key into nested selects even if omitted. Relation field names resolve from schema / DMMF meta (`schemaPath` defaults to `prisma/schema.prisma`).
|
|
192
|
+
|
|
193
|
+
## Row locks
|
|
194
|
+
|
|
195
|
+
Repo must declare `lock`. Call must pass `tx`. Default mode is `noKeyUpdate`. `skipLocked` cannot combine with `nowait`. Prefer short transactions.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
await prisma.$transaction(async (tx) => {
|
|
199
|
+
const row = await wallets.getById({
|
|
200
|
+
tx,
|
|
201
|
+
id,
|
|
202
|
+
select: { id: true, balance: true },
|
|
203
|
+
lock: { mode: 'update' },
|
|
204
|
+
});
|
|
205
|
+
await wallets.updateById({
|
|
206
|
+
tx,
|
|
207
|
+
id,
|
|
208
|
+
data: { balance: row!.balance - amount },
|
|
209
|
+
invalidate: 'none',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
await wallets.invalidateCache({ id });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## CLI and ESLint
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx prismakit generate <name> --cache
|
|
219
|
+
npx prismakit generate <name> --cache --full --route <path>
|
|
220
|
+
npx prismakit validate --auto-register
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
// eslint.config.mjs
|
|
225
|
+
import prismakit from '@prismakit/eslint-plugin';
|
|
226
|
+
export default [prismakit.configs.recommended];
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Allowed Prisma usage: `**/repositories/**`, `**/infrastructure/prisma/**`. Rules: `no-prisma-service-outside-repos`, `no-direct-prisma-delegate`, `require-transaction-service`.
|
|
230
|
+
|
|
231
|
+
## Clean code
|
|
232
|
+
|
|
233
|
+
- One repository per Prisma model, file `*.repository.ts` under `repositories/`.
|
|
234
|
+
- Select presets as `satisfies Prisma.XSelect`: `minimal` (no cache), `general` (API, cacheable), `withPassword` (auth only, never cached). Keep presets next to the repo; controllers never build selects.
|
|
235
|
+
- Named TTL constants (`const DAY = 86_400`), not magic numbers scattered in call sites.
|
|
236
|
+
- Pass `tx` into **every** repo call in a unit of work. Do not mix cached reads with half-committed writes.
|
|
237
|
+
- Tests: `@prismakit/memory` `MemoryCacheAdapter`. Production: `@prismakit/redis`.
|
|
238
|
+
|
|
239
|
+
## Anti-patterns (BAD → GOOD)
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// BAD — Prisma in a service
|
|
243
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
244
|
+
await this.prisma.user.findUnique({ where: { id } });
|
|
245
|
+
await this.prisma.$transaction(async (tx) => { /* ... */ });
|
|
246
|
+
await this.prisma.post.findUnique({ include: { author: true } });
|
|
247
|
+
await users.getFirst({ where: { email }, select: { id: true }, setCache: true });
|
|
248
|
+
await users.getById({ id, select: { password: true }, setCache: true });
|
|
249
|
+
await wallets.getById({ id, select: { id: true }, lock: { mode: 'update' } }); // no tx
|
|
250
|
+
await users.updateById({ tx, id, data }); // auto-invalidate inside tx
|
|
251
|
+
|
|
252
|
+
// GOOD
|
|
253
|
+
constructor(private readonly users: UserRepository) {}
|
|
254
|
+
await this.users.getById({ id, select: { id: true, email: true }, setCache: true });
|
|
255
|
+
await prisma.$transaction(async (tx) => {
|
|
256
|
+
await this.users.updateById({ tx, id, data, invalidate: 'none' });
|
|
257
|
+
});
|
|
258
|
+
await this.users.invalidateCache({ id });
|
|
259
|
+
await this.posts.getById({
|
|
260
|
+
id,
|
|
261
|
+
select: { id: true, author: { select: { id: true, name: true } } },
|
|
262
|
+
setCache: true,
|
|
263
|
+
});
|
|
264
|
+
await this.users.getFirst({ where: { email }, select: { id: true } }); // no setCache
|
|
265
|
+
await this.users.getFirst({
|
|
266
|
+
where: { email },
|
|
267
|
+
select: { id: true, password: true }, // auth; never setCache
|
|
268
|
+
});
|
|
269
|
+
await prisma.$transaction(async (tx) => {
|
|
270
|
+
await this.wallets.getById({
|
|
271
|
+
tx, id, select: { id: true, balance: true }, lock: { mode: 'update' },
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Before you finish
|
|
277
|
+
|
|
278
|
+
Copy and complete [review-checklist.md](review-checklist.md).
|
|
279
|
+
|
|
280
|
+
- Method/options detail: [reference.md](reference.md)
|
|
281
|
+
- End-to-end snippets: [examples.md](examples.md)
|
|
282
|
+
- NestJS wiring: skill `prismakit-nestjs`
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# PrismaKit core examples
|
|
2
|
+
|
|
3
|
+
Copy-paste patterns for `@prismakit/core`. Contract: [SKILL.md](SKILL.md).
|
|
4
|
+
|
|
5
|
+
## 1. New repository from scratch
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// src/modules/users/repositories/user.repository.ts
|
|
9
|
+
import { Prisma } from '@prisma/client';
|
|
10
|
+
import { createRepository } from '@prismakit/core';
|
|
11
|
+
|
|
12
|
+
const DAY = 86_400;
|
|
13
|
+
|
|
14
|
+
export const userSelectPresets = {
|
|
15
|
+
minimal: { id: true } satisfies Prisma.UserSelect,
|
|
16
|
+
general: { id: true, email: true, name: true } satisfies Prisma.UserSelect,
|
|
17
|
+
withPassword: {
|
|
18
|
+
id: true,
|
|
19
|
+
email: true,
|
|
20
|
+
password: true,
|
|
21
|
+
} satisfies Prisma.UserSelect,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const UserRepoClass = createRepository({
|
|
25
|
+
model: 'user',
|
|
26
|
+
scalarFields: Prisma.UserScalarFieldEnum,
|
|
27
|
+
cache: {
|
|
28
|
+
ttl: DAY,
|
|
29
|
+
nullTtl: 60,
|
|
30
|
+
sensitiveFields: ['password'],
|
|
31
|
+
methods: {
|
|
32
|
+
getFirst: { enabled: false },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
lock: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type UserRepository = InstanceType<typeof UserRepoClass>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// src/infrastructure/prisma/repos.ts
|
|
43
|
+
import { PrismaClient } from '@prisma/client';
|
|
44
|
+
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
45
|
+
import { UserRepoClass } from '../../modules/users/repositories/user.repository';
|
|
46
|
+
|
|
47
|
+
const prisma = new PrismaClient();
|
|
48
|
+
const cache = new RedisCacheAdapter({
|
|
49
|
+
url: process.env.REDIS_URL,
|
|
50
|
+
prefix: process.env.CACHE_PREFIX ?? 'myapp',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const users = new UserRepoClass({ prisma, cache });
|
|
54
|
+
export { prisma };
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 2. User-facing read vs uniqueness check
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { users } from '../../infrastructure/prisma/repos';
|
|
61
|
+
import { userSelectPresets } from './repositories/user.repository';
|
|
62
|
+
|
|
63
|
+
export async function getProfile(id: string) {
|
|
64
|
+
return users.getThrowById({
|
|
65
|
+
id,
|
|
66
|
+
select: userSelectPresets.general,
|
|
67
|
+
setCache: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function assertEmailFree(email: string) {
|
|
72
|
+
const existing = await users.getFirst({
|
|
73
|
+
where: { email },
|
|
74
|
+
select: userSelectPresets.minimal,
|
|
75
|
+
// never setCache on uniqueness
|
|
76
|
+
});
|
|
77
|
+
if (existing) throw new Error('email taken');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function verifyLogin(email: string) {
|
|
81
|
+
return users.getFirst({
|
|
82
|
+
where: { email },
|
|
83
|
+
select: userSelectPresets.withPassword,
|
|
84
|
+
// never cache password selects
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 3. Paginated list with cache tags
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const page = await products.getManyPaginate({
|
|
93
|
+
where: { categoryId, status: 'ACTIVE' },
|
|
94
|
+
select: { id: true, name: true, price: true },
|
|
95
|
+
orderBy: { createdAt: 'desc' },
|
|
96
|
+
page: 1,
|
|
97
|
+
pageSize: 20,
|
|
98
|
+
setCache: true,
|
|
99
|
+
cacheTags: [`category:${categoryId}`],
|
|
100
|
+
});
|
|
101
|
+
// page.data, page.meta.totalItems, page.meta.totalPages
|
|
102
|
+
|
|
103
|
+
await products.updateById({
|
|
104
|
+
id,
|
|
105
|
+
data: { price: 1999 },
|
|
106
|
+
select: { id: true },
|
|
107
|
+
tags: [`category:${categoryId}`],
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 4. Transaction + invalidate after commit
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { prisma, users } from '../../infrastructure/prisma/repos';
|
|
115
|
+
|
|
116
|
+
export async function renameUser(id: string, name: string) {
|
|
117
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
118
|
+
return users.updateById({
|
|
119
|
+
tx,
|
|
120
|
+
id,
|
|
121
|
+
data: { name },
|
|
122
|
+
select: { id: true, name: true },
|
|
123
|
+
invalidate: 'none',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
await users.invalidateCache({ id });
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Prefer wrapping `$transaction` + `afterCommit` in one app helper so every call site stays consistent.
|
|
132
|
+
|
|
133
|
+
## 5. Transfer with row lock
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
export async function transfer(fromId: string, toId: string, amount: number) {
|
|
137
|
+
const moved = await prisma.$transaction(async (tx) => {
|
|
138
|
+
const from = await wallets.getById({
|
|
139
|
+
tx,
|
|
140
|
+
id: fromId,
|
|
141
|
+
select: { id: true, balance: true },
|
|
142
|
+
lock: { mode: 'update' },
|
|
143
|
+
});
|
|
144
|
+
const to = await wallets.getById({
|
|
145
|
+
tx,
|
|
146
|
+
id: toId,
|
|
147
|
+
select: { id: true, balance: true },
|
|
148
|
+
lock: { mode: 'update' },
|
|
149
|
+
});
|
|
150
|
+
if (!from || !to) throw new Error('wallet missing');
|
|
151
|
+
if (from.balance < amount) throw new Error('insufficient funds');
|
|
152
|
+
|
|
153
|
+
await wallets.updateById({
|
|
154
|
+
tx,
|
|
155
|
+
id: fromId,
|
|
156
|
+
data: { balance: from.balance - amount },
|
|
157
|
+
invalidate: 'none',
|
|
158
|
+
});
|
|
159
|
+
await wallets.updateById({
|
|
160
|
+
tx,
|
|
161
|
+
id: toId,
|
|
162
|
+
data: { balance: to.balance + amount },
|
|
163
|
+
invalidate: 'none',
|
|
164
|
+
});
|
|
165
|
+
return { fromId, toId, amount };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await wallets.invalidateCache({ id: fromId });
|
|
169
|
+
await wallets.invalidateCache({ id: toId });
|
|
170
|
+
return moved;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 6. Nested select (auto-compose)
|
|
175
|
+
|
|
176
|
+
Related repositories must be constructed and registered before the read.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const post = await posts.getThrowById({
|
|
180
|
+
id,
|
|
181
|
+
select: {
|
|
182
|
+
id: true,
|
|
183
|
+
title: true,
|
|
184
|
+
author: { select: { name: true } }, // PK injected; no Prisma include
|
|
185
|
+
comments: {
|
|
186
|
+
select: {
|
|
187
|
+
id: true,
|
|
188
|
+
body: true,
|
|
189
|
+
author: { select: { name: true } },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
setCache: true,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 7. Unit test with MemoryCacheAdapter
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { createRepository } from '@prismakit/core';
|
|
201
|
+
import { MemoryCacheAdapter } from '@prismakit/memory';
|
|
202
|
+
|
|
203
|
+
const UserRepo = createRepository({
|
|
204
|
+
model: 'user',
|
|
205
|
+
scalarFields: { id: 'id', email: 'email', name: 'name' },
|
|
206
|
+
cache: { ttl: 60 },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const prisma = {
|
|
210
|
+
user: {
|
|
211
|
+
findUnique: async ({ where }: { where: { id: string } }) =>
|
|
212
|
+
where.id === 'u1' ? { id: 'u1', email: 'a@b.c', name: 'Ada' } : null,
|
|
213
|
+
findUniqueOrThrow: async ({ where }: { where: { id: string } }) => {
|
|
214
|
+
if (where.id !== 'u1') throw new Error('missing');
|
|
215
|
+
return { id: 'u1', email: 'a@b.c', name: 'Ada' };
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const cache = new MemoryCacheAdapter({ prefix: 'test' });
|
|
221
|
+
const users = new UserRepo({ prisma, cache });
|
|
222
|
+
|
|
223
|
+
const select = { id: true, email: true, name: true };
|
|
224
|
+
|
|
225
|
+
const a = await users.getThrowById({ id: 'u1', select, setCache: true });
|
|
226
|
+
const b = await users.getThrowById({ id: 'u1', select, setCache: true }); // cache hit
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## 8. Scaffold with CLI
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npx prismakit generate product --cache
|
|
233
|
+
npx prismakit generate product --cache --full --route products
|
|
234
|
+
npx prismakit generate product --prisma-import src/infrastructure/prisma/client --dry-run
|
|
235
|
+
npx prismakit validate --auto-register
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
After repo-only generate: register the class (Nest `providers`, or `new RepoClass({ prisma, cache })` in plain Node). After `--full`: import the feature module in `app.module.ts`.
|