@prismakit/cli 3.2.1 → 4.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/dist/bin.cjs +8 -9
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-MY5VVL5C.js → chunk-MCTLXSRF.js} +9 -10
- package/dist/chunk-MCTLXSRF.js.map +1 -0
- package/dist/index.cjs +8 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +12 -12
- package/rules/data-access.mdc +1 -1
- package/skills/prismakit/SKILL.md +28 -22
- package/skills/prismakit/examples.md +2 -4
- package/skills/prismakit/reference.md +30 -49
- package/skills/prismakit/review-checklist.md +2 -3
- package/skills/prismakit-nestjs/SKILL.md +18 -15
- package/skills/prismakit-nestjs/examples.md +30 -17
- package/skills/prismakit-nestjs/reference.md +13 -53
- package/skills/prismakit-nestjs/review-checklist.md +1 -2
- package/src/__tests__/naming.spec.ts +5 -2
- package/src/commands/skills.ts +0 -1
- package/src/templates.ts +7 -10
- package/dist/chunk-MY5VVL5C.js.map +0 -1
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
|
|
12
12
|
Framework-agnostic Prisma repository kit. Not a Prisma fork. Only repositories talk to Prisma.
|
|
13
13
|
|
|
14
|
-
NestJS apps: also load skill `prismakit-nestjs` after this contract.
|
|
14
|
+
**Line:** 4.0 (pre-stable). NestJS apps: also load skill `prismakit-nestjs` after this contract.
|
|
15
15
|
|
|
16
16
|
## Non-negotiables
|
|
17
17
|
|
|
@@ -29,10 +29,10 @@ Violations are bugs. Enforce with `@prismakit/eslint-plugin` + this skill.
|
|
|
29
29
|
|
|
30
30
|
| Required | How |
|
|
31
31
|
|----------|-----|
|
|
32
|
-
| Reads/writes | `*Repository` from `createRepository` / Nest
|
|
32
|
+
| Reads/writes | `*Repository` from `createRepository` / Nest `createDefineRepo` |
|
|
33
33
|
| Tx writes | `invalidate: 'none'` then `invalidateCache` after commit |
|
|
34
34
|
| User-facing reads | `setCache: true` when the repo has cache config |
|
|
35
|
-
| Relations in `select` | `model` + `
|
|
35
|
+
| Relations in `select` | `model` + Prisma meta loaded (`loadPrismaMetaFromSchema` / `loadPrismaMetaFromDmmf` or Nest `schemaPath` / `dmmf`) |
|
|
36
36
|
| ESLint | `prismakit.configs.recommended` |
|
|
37
37
|
|
|
38
38
|
## Layers
|
|
@@ -47,7 +47,7 @@ Helpers may inject repositories — never the Prisma client.
|
|
|
47
47
|
|
|
48
48
|
| Task | Use |
|
|
49
49
|
|------|-----|
|
|
50
|
-
| New repository | `createRepository` (core) or Nest `
|
|
50
|
+
| New repository | `createRepository` (core) or Nest `defineAppRepo` — see `prismakit-nestjs` |
|
|
51
51
|
| User-facing get by id | `getThrowById` / `getById` + `setCache: true` |
|
|
52
52
|
| Existence / uniqueness / auth | `getFirst` — **no** `setCache` |
|
|
53
53
|
| List | `getMany` + `setCache: true` + optional `cacheTags` |
|
|
@@ -65,15 +65,18 @@ Helpers may inject repositories — never the Prisma client.
|
|
|
65
65
|
|
|
66
66
|
```typescript
|
|
67
67
|
import { PrismaClient } from '@prisma/client';
|
|
68
|
-
import { createRepository } from '@prismakit/core';
|
|
68
|
+
import { createRepository, loadPrismaMetaFromSchema } from '@prismakit/core';
|
|
69
69
|
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
70
70
|
|
|
71
|
+
loadPrismaMetaFromSchema('prisma/schema.prisma');
|
|
72
|
+
// Prisma 5/6: loadPrismaMetaFromDmmf(Prisma.dmmf)
|
|
73
|
+
|
|
71
74
|
const DAY = 86_400;
|
|
72
75
|
|
|
73
76
|
const UserRepoClass = createRepository({
|
|
74
77
|
model: 'user',
|
|
75
78
|
cache: { ttl: DAY, nullTtl: 60, sensitiveFields: ['password'] },
|
|
76
|
-
lock: true, // table + columns
|
|
79
|
+
lock: true, // table + columns from Prisma meta
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
const prisma = new PrismaClient();
|
|
@@ -81,19 +84,16 @@ const cache = new RedisCacheAdapter({ prefix: 'myapp' });
|
|
|
81
84
|
export const users = new UserRepoClass({ prisma, cache });
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
`defineRepository`
|
|
87
|
+
Public factory: **`createRepository` only** (no `defineRepository` / `createPrismaRepository` aliases).
|
|
85
88
|
|
|
86
|
-
**NestJS apps:** use `createDefineRepo` / `defineAppRepo`
|
|
89
|
+
**NestJS apps:** use `createDefineRepo` / `defineAppRepo` — see skill `prismakit-nestjs`.
|
|
87
90
|
|
|
88
91
|
| Option | Description |
|
|
89
92
|
|--------|-------------|
|
|
90
|
-
| `model` | Prisma client key (`prisma.user` → `'user'`).
|
|
91
|
-
| `scalarFields` | Usually `Prisma.XScalarFieldEnum`. **Optional** when `schemaPath` / DMMF meta is loaded (default since 3.1). |
|
|
93
|
+
| `model` | Prisma client key (`prisma.user` → `'user'`). **Required.** |
|
|
92
94
|
| `cache` | `CacheOptions` or `true` (uses app defaults when bound via `createDefineRepo`). |
|
|
93
|
-
| `lock` | `true`
|
|
94
|
-
| `
|
|
95
|
-
| `schemaPath` | Path to `schema.prisma` when meta is not loaded globally. Default: `prisma/schema.prisma`. |
|
|
96
|
-
| `getDelegate` | Optional. Defaults to `(c) => c[model]`. |
|
|
95
|
+
| `lock` | `true` (from meta) or `{ tableName, columns? }`. |
|
|
96
|
+
| `toPayload` | Optional payload mapper (default identity). |
|
|
97
97
|
|
|
98
98
|
Put files under `**/repositories/**`. Keep Prisma client construction under `**/infrastructure/prisma/**`.
|
|
99
99
|
|
|
@@ -123,7 +123,8 @@ await users.getFirst({
|
|
|
123
123
|
| `getMany` | array (`take` / `skip` / `orderBy`) |
|
|
124
124
|
| `getManyPaginate` | `{ data, meta: { page, pageSize, totalItems, totalPages } }` |
|
|
125
125
|
| `getManyCursor` | `{ data, nextCursor, hasMore }` |
|
|
126
|
-
| `
|
|
126
|
+
| `getThrowFirst` | first match; throws if missing |
|
|
127
|
+
| `count` / `exists` | `number` / `boolean` |
|
|
127
128
|
| `aggregate` / `groupBy` | Prisma delegate results |
|
|
128
129
|
|
|
129
130
|
`id` is `string` or `Record<string, string>` for composite PKs (object form for `@@id([a,b])`).
|
|
@@ -134,8 +135,11 @@ await users.getFirst({
|
|
|
134
135
|
|
|
135
136
|
| Method | Default `invalidate` |
|
|
136
137
|
|--------|----------------------|
|
|
137
|
-
| `create` / `createMany` | `queries` |
|
|
138
|
-
| `updateById` / `
|
|
138
|
+
| `create` / `createMany` / `createManyAndReturn` | `queries` |
|
|
139
|
+
| `updateById` / `update` / `updateMany` / `updateManyAndReturn` | `all` |
|
|
140
|
+
| `upsert` / `upsertMany` | `all` |
|
|
141
|
+
| `deleteById` / `delete` / `deleteMany` | `all` |
|
|
142
|
+
| `queryRaw` / `executeRaw` | none (raw SQL — no cache) |
|
|
139
143
|
|
|
140
144
|
```typescript
|
|
141
145
|
await users.updateById({
|
|
@@ -163,7 +167,7 @@ If Redis is down, `RedisCacheAdapter` **fails open** — queries still hit Prism
|
|
|
163
167
|
| Auth, uniqueness, JWT lookup | omit / `false` |
|
|
164
168
|
| Inside `tx` | ignored |
|
|
165
169
|
|
|
166
|
-
Repository `cache` is the source of truth.
|
|
170
|
+
Repository `cache` is the source of truth. There is no Nest `cacheModels` allowlist in 4.0.
|
|
167
171
|
|
|
168
172
|
`setCache` / `cacheTags` / `invalidate` / `invalidateCache` exist on the type **only** when the repo has `cache` config. Do not force them on uncached repos.
|
|
169
173
|
|
|
@@ -196,9 +200,9 @@ await posts.getThrowById({
|
|
|
196
200
|
});
|
|
197
201
|
```
|
|
198
202
|
|
|
199
|
-
Requirements: source repo has `model`;
|
|
203
|
+
Requirements: source repo has `model`; Prisma meta loaded; related model repos are registered.
|
|
200
204
|
|
|
201
|
-
AutoComposer injects the target primary key into nested selects even if omitted. Relation field names resolve from schema / DMMF meta
|
|
205
|
+
AutoComposer injects the target primary key into nested selects even if omitted. Relation field names resolve from schema / DMMF meta.
|
|
202
206
|
|
|
203
207
|
## Row locks
|
|
204
208
|
|
|
@@ -240,8 +244,10 @@ Allowed Prisma usage: `**/repositories/**`, `**/infrastructure/prisma/**`. Rules
|
|
|
240
244
|
|
|
241
245
|
## Observability
|
|
242
246
|
|
|
243
|
-
- Core: `setTelemetry({ enabled, onEvent })`
|
|
244
|
-
-
|
|
247
|
+
- Core: `setTelemetry({ enabled, onEvent, slowThreshold })`
|
|
248
|
+
- Nest: `telemetry: { enabled, slowThreshold, onSlowQuery, onEvent }`
|
|
249
|
+
- Optional: `@prismakit/opentelemetry` → `createPrismaKitTelemetry({ slowThreshold })`
|
|
250
|
+
- Events: `cache.hit` / `cache.miss` / `cache.bypass` / `cache.invalidate` / `cache.error`, `compose.*`, `lock.*`, `stampede.*`, `query.complete` / `query.slow`
|
|
245
251
|
|
|
246
252
|
## Clean code
|
|
247
253
|
|
|
@@ -6,7 +6,7 @@ Copy-paste patterns for `@prismakit/core`. Contract: [SKILL.md](SKILL.md).
|
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
// src/modules/users/repositories/user.repository.ts
|
|
9
|
-
import { Prisma } from '@prisma/client';
|
|
9
|
+
import { Prisma } from '@prisma/client'; // or generated client path for Prisma 7
|
|
10
10
|
import { createRepository } from '@prismakit/core';
|
|
11
11
|
|
|
12
12
|
const DAY = 86_400;
|
|
@@ -23,7 +23,6 @@ export const userSelectPresets = {
|
|
|
23
23
|
|
|
24
24
|
export const UserRepoClass = createRepository({
|
|
25
25
|
model: 'user',
|
|
26
|
-
scalarFields: Prisma.UserScalarFieldEnum,
|
|
27
26
|
cache: {
|
|
28
27
|
ttl: DAY,
|
|
29
28
|
nullTtl: 60,
|
|
@@ -40,7 +39,7 @@ export type UserRepository = InstanceType<typeof UserRepoClass>;
|
|
|
40
39
|
|
|
41
40
|
```typescript
|
|
42
41
|
// src/infrastructure/prisma/repos.ts
|
|
43
|
-
import { PrismaClient } from '@prisma/client';
|
|
42
|
+
import { PrismaClient } from '@prisma/client'; // or generated client for Prisma 7
|
|
44
43
|
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
45
44
|
import { UserRepoClass } from '../../modules/users/repositories/user.repository';
|
|
46
45
|
|
|
@@ -202,7 +201,6 @@ import { MemoryCacheAdapter } from '@prismakit/memory';
|
|
|
202
201
|
|
|
203
202
|
const UserRepo = createRepository({
|
|
204
203
|
model: 'user',
|
|
205
|
-
scalarFields: { id: 'id', email: 'email', name: 'name' },
|
|
206
204
|
cache: { ttl: 60 },
|
|
207
205
|
});
|
|
208
206
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# PrismaKit core reference
|
|
2
2
|
|
|
3
|
-
API surface for `@prismakit/core`
|
|
3
|
+
API surface for `@prismakit/core` **4.0** (pre-stable). Read [SKILL.md](SKILL.md) first.
|
|
4
4
|
|
|
5
5
|
## Packages
|
|
6
6
|
|
|
7
7
|
| Package | Role |
|
|
8
8
|
|---------|------|
|
|
9
9
|
| `@prismakit/core` | `createRepository`, AutoComposer, locks, pagination, `CacheAdapter` |
|
|
10
|
-
| `@prismakit/redis` | `RedisCacheAdapter` |
|
|
10
|
+
| `@prismakit/redis` | `RedisCacheAdapter`, `createRedisJsonReviver` |
|
|
11
11
|
| `@prismakit/memory` | `MemoryCacheAdapter` (tests / local) |
|
|
12
12
|
| `@prismakit/opentelemetry` | Map telemetry → OTel metrics/spans |
|
|
13
13
|
| `@prismakit/cli` | `prismakit generate / validate / skills` |
|
|
14
14
|
| `@prismakit/eslint-plugin` | Repository-only data-access rules |
|
|
15
15
|
|
|
16
|
-
Node ≥ 20.
|
|
16
|
+
Node ≥ 20. Install:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
pnpm add @prismakit/core
|
|
@@ -29,7 +29,7 @@ pnpm add -D @prismakit/eslint-plugin @prismakit/cli
|
|
|
29
29
|
createRepository(options) → new RepoClass(deps: RepositoryDeps)
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Public factory: **`createRepository` only**.
|
|
33
33
|
|
|
34
34
|
`RepositoryDeps`: `{ prisma, cache?, registry?, autoCompose? }`.
|
|
35
35
|
|
|
@@ -37,14 +37,12 @@ Aliases: `defineRepository`, `createPrismaRepository`.
|
|
|
37
37
|
|
|
38
38
|
| Field | Type | Notes |
|
|
39
39
|
|-------|------|-------|
|
|
40
|
-
| `model` | `string` | Client key. Required
|
|
41
|
-
| `scalarFields` | `Record<string, string>` | `Prisma.XScalarFieldEnum`; inferred from meta if omitted. |
|
|
40
|
+
| `model` | `string` | Client key. **Required.** |
|
|
42
41
|
| `cache` | `CacheOptions \| true` | `true` → `{ ttl: 86400, sensitiveFields: ['password'] }`. |
|
|
43
|
-
| `lock` | `true \|
|
|
44
|
-
| `
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
| `toPayload` | `(data) => payload` | Default identity. Prefer typed factories over this. |
|
|
42
|
+
| `lock` | `true \| RepositoryLockConfig` | `true` resolves table/columns from Prisma meta. |
|
|
43
|
+
| `toPayload` | `(data) => payload` | Default identity. |
|
|
44
|
+
|
|
45
|
+
Scalars, primary key, and relations come from Prisma meta (`loadPrismaMetaFromSchema` / `loadPrismaMetaFromDmmf`).
|
|
48
46
|
|
|
49
47
|
`RepositoryLockConfig`: `{ tableName: string; columns?: Record<string, string> }`.
|
|
50
48
|
|
|
@@ -58,12 +56,13 @@ All methods accept optional `tx`. Cached repos also accept cache fields (see bel
|
|
|
58
56
|
|--------|------------|---------|
|
|
59
57
|
| `getById` | `id`, `select?`, `lock?`, `setCache?` | `T \| null` |
|
|
60
58
|
| `getThrowById` | same | `T` (throws if missing) |
|
|
61
|
-
| `getFirst` | `where?`, `select?`, `lock?`, `setCache?`, `cacheTags?` | `T \| null` |
|
|
59
|
+
| `getFirst` | `where?`, `select?`, `lock?`, `setCache?`, `cacheTags?`, `orderBy?` | `T \| null` |
|
|
60
|
+
| `getThrowFirst` | same as `getFirst` | `T` (throws if missing) |
|
|
62
61
|
| `getMany` | `where?`, `select?`, `orderBy?`, `take?`, `skip?`, `lock?`, `setCache?`, `cacheTags?` | `T[]` |
|
|
63
62
|
| `getManyPaginate` | `where?`, `select?`, `orderBy?`, `page?`, `pageSize?`, `setCache?`, `cacheTags?` | `PaginatedResult<T>` |
|
|
64
63
|
| `getManyCursor` | `where?`, `select?`, `orderBy?`, `cursor?`, `take?`, `skip?`, `setCache?`, `cacheTags?` | `CursorPage<T>`; with `cursor`, default `skip: 1` |
|
|
65
|
-
| `count` | `where?`, `
|
|
66
|
-
| `exists` | `where?`, `setCache?`, `cacheTags?` | `
|
|
64
|
+
| `count` | `where?`, `setCache?`, `cacheTags?` | `number` |
|
|
65
|
+
| `exists` | `where?`, `setCache?`, `cacheTags?` | `boolean` |
|
|
67
66
|
| `aggregate` | Prisma aggregate args + `setCache?`, `cacheTags?` | delegate result |
|
|
68
67
|
| `groupBy` | Prisma groupBy args + `setCache?`, `cacheTags?` | delegate result |
|
|
69
68
|
|
|
@@ -86,6 +85,13 @@ All methods accept optional `tx`. Cached repos also accept cache fields (see bel
|
|
|
86
85
|
| `upsert` | `where`, `create`, `update`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
|
|
87
86
|
| `deleteById` | `id`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
|
|
88
87
|
| `deleteMany` | `where`, `invalidate?`, `tags?` | `all` | `{ count }` |
|
|
88
|
+
| `update` | `where`, `data`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
|
|
89
|
+
| `delete` | `where`, `select?`, `invalidate?`, `tags?` | `all` | `T` |
|
|
90
|
+
| `createManyAndReturn` | `data[]`, `select?`, `skipDuplicates?`, `invalidate?`, `tags?` | `queries` | `T[]` |
|
|
91
|
+
| `updateManyAndReturn` | `where`, `data`, `select?`, `invalidate?`, `tags?` | `all` | `T[]` |
|
|
92
|
+
| `upsertMany` | `data[]`, `skipDuplicates?`, `invalidate?`, `tags?` | `all` | `{ count }` |
|
|
93
|
+
| `queryRaw` | `sql`, `...params` | — | raw result |
|
|
94
|
+
| `executeRaw` | `sql`, `...params` | — | `number` (affected rows) |
|
|
89
95
|
|
|
90
96
|
Mutation `tags`: `string[] | null | undefined | ((result) => string[] | null | undefined)`.
|
|
91
97
|
|
|
@@ -112,10 +118,10 @@ await repo.invalidateCache({ id?: string; tags?: string[] });
|
|
|
112
118
|
| `ttl` | `86400` | Entity TTL (seconds). |
|
|
113
119
|
| `nullTtl` | — | Negative cache for null results. |
|
|
114
120
|
| `sensitiveFields` | `['password']` | Selects containing these never cache. |
|
|
115
|
-
| `methods` | — | Per-method `{ enabled?, ttl? }
|
|
121
|
+
| `methods` | — | Per-method `{ enabled?, ttl? }`. |
|
|
116
122
|
| `defaultSetCache` | `false` | Reads cache unless caller passes `setCache: false`. |
|
|
117
123
|
| `stampede` | see below | Per-repo stampede overrides. |
|
|
118
|
-
| `
|
|
124
|
+
| `strictInvalidation` | `false` | When true, invalidation failures rethrow. |
|
|
119
125
|
|
|
120
126
|
### `InvalidateMode`
|
|
121
127
|
|
|
@@ -125,11 +131,6 @@ await repo.invalidateCache({ id?: string; tags?: string[] });
|
|
|
125
131
|
| `entity` | Entity keys only |
|
|
126
132
|
| `queries` | Query index only |
|
|
127
133
|
| `none` | Skip — **required inside transactions** |
|
|
128
|
-
| `stale` | Core treats like entity+queries (adapter may layer SWR) |
|
|
129
|
-
|
|
130
|
-
### Allowlist
|
|
131
|
-
|
|
132
|
-
`setRegisteredCacheModels(['user', 'product'])`. Empty/unset = fail-open. When set, a repo with `cache` whose `model` is missing throws at init.
|
|
133
134
|
|
|
134
135
|
### Key schema
|
|
135
136
|
|
|
@@ -142,7 +143,7 @@ await repo.invalidateCache({ id?: string; tags?: string[] });
|
|
|
142
143
|
{prefix}:v2:repo:{model}:t:{tag}:__idx
|
|
143
144
|
```
|
|
144
145
|
|
|
145
|
-
Redis payloads use tagged JSON for `Date`, `BigInt`, `Bytes`, and `Decimal
|
|
146
|
+
Redis payloads use tagged JSON for `Date`, `BigInt`, `Bytes`, and `Decimal`. Custom revive: `createRedisJsonReviver` from `@prismakit/redis`.
|
|
146
147
|
|
|
147
148
|
### Debug
|
|
148
149
|
|
|
@@ -162,8 +163,6 @@ Hits/misses/bypasses via `cacheDebugStorage` from `@prismakit/core`.
|
|
|
162
163
|
| `backoff` | `'exponential'` (`'fixed'` also valid) |
|
|
163
164
|
| `totalTimeoutMs` | `3000` |
|
|
164
165
|
|
|
165
|
-
In-process `singleflight` is also used. If Redis is down, fail open.
|
|
166
|
-
|
|
167
166
|
### Adapters
|
|
168
167
|
|
|
169
168
|
**Redis** (`@prismakit/redis`):
|
|
@@ -185,11 +184,11 @@ new RedisCacheAdapter({
|
|
|
185
184
|
new MemoryCacheAdapter({ prefix: 'test', maxSize: 1000, defaultTtl: 300 });
|
|
186
185
|
```
|
|
187
186
|
|
|
188
|
-
Custom: implement `CacheAdapter
|
|
187
|
+
Custom: implement `CacheAdapter`. Prefer fail-open `safe*` semantics.
|
|
189
188
|
|
|
190
189
|
## Auto-compose
|
|
191
190
|
|
|
192
|
-
`splitSelect` keeps scalars (+ FK fields from
|
|
191
|
+
`splitSelect` keeps scalars (+ FK fields from meta) for the Prisma query. Relation keys load via the target repository. Target PK is always injected into the nested select.
|
|
193
192
|
|
|
194
193
|
Load meta once at bootstrap:
|
|
195
194
|
|
|
@@ -200,8 +199,6 @@ loadPrismaMetaFromDmmf(Prisma.dmmf); // Prisma 5/6
|
|
|
200
199
|
loadPrismaMetaFromSchema('prisma/schema.prisma'); // Prisma 7 (no Prisma.dmmf)
|
|
201
200
|
```
|
|
202
201
|
|
|
203
|
-
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).
|
|
204
|
-
|
|
205
202
|
Validate: `npx prismakit validate --auto-register` or `assertSelectComposeValid`.
|
|
206
203
|
|
|
207
204
|
### `ComposeOptions` (global via `setComposeOptions`)
|
|
@@ -211,7 +208,6 @@ Validate: `npx prismakit validate --auto-register` or `assertSelectComposeValid`
|
|
|
211
208
|
| `maxDepth` | `10` | Max relation nesting. |
|
|
212
209
|
| `parallel` | `true` | Same-level relations via `Promise.all`. |
|
|
213
210
|
| `setCache` | `true` | Nested fetches pass `setCache: true` unless parent has `tx` / `setCache: false`. |
|
|
214
|
-
| `tx` | — | Per-call only; forwarded to nested `getMany`. |
|
|
215
211
|
|
|
216
212
|
Related repos must be registered on `RepositoryRegistry`.
|
|
217
213
|
|
|
@@ -227,7 +223,7 @@ Related repos must be registered on `RepositoryRegistry`.
|
|
|
227
223
|
| `nowait` | Fail immediately if locked. |
|
|
228
224
|
| `skipLocked` | Skip locked rows. **Cannot** combine with `nowait`. |
|
|
229
225
|
|
|
230
|
-
`lock: true`
|
|
226
|
+
`lock: true` resolves via Prisma meta. Explicit: `{ tableName, columns? }`.
|
|
231
227
|
|
|
232
228
|
## Telemetry
|
|
233
229
|
|
|
@@ -237,12 +233,13 @@ import { setTelemetry } from '@prismakit/core';
|
|
|
237
233
|
setTelemetry({
|
|
238
234
|
enabled: true,
|
|
239
235
|
onEvent: (event) => { /* metrics */ },
|
|
236
|
+
slowThreshold: 500, // emits query.slow for queries ≥ this ms
|
|
240
237
|
});
|
|
241
238
|
```
|
|
242
239
|
|
|
243
240
|
| Type | When |
|
|
244
241
|
|------|------|
|
|
245
|
-
| `cache.hit` / `cache.miss` / `cache.bypass` / `cache.invalidate` | Cache-aside path |
|
|
242
|
+
| `cache.hit` / `cache.miss` / `cache.bypass` / `cache.invalidate` / `cache.error` | Cache-aside path |
|
|
246
243
|
| `compose.start` / `compose.complete` | Auto-compose (`queryCount`, `durationMs`) |
|
|
247
244
|
| `lock.acquired` / `lock.waited` / `lock.timeout` | Row locks |
|
|
248
245
|
| `stampede.locked` / `stampede.waited` / `stampede.fallthrough` | Stampede protection |
|
|
@@ -256,10 +253,6 @@ npx prismakit validate [--schema <path>] [--auto-register] [--no-assert]
|
|
|
256
253
|
npx prismakit help
|
|
257
254
|
```
|
|
258
255
|
|
|
259
|
-
Default generate output: `src/modules/<kebab>/repositories/<kebab>.repository.ts`.
|
|
260
|
-
|
|
261
|
-
`--full` emits Nest module + controller + service + select/where types. Register `*Module` in `app.module.ts` afterwards.
|
|
262
|
-
|
|
263
256
|
## ESLint
|
|
264
257
|
|
|
265
258
|
```js
|
|
@@ -267,8 +260,6 @@ import prismakit from '@prismakit/eslint-plugin';
|
|
|
267
260
|
export default [prismakit.configs.recommended];
|
|
268
261
|
```
|
|
269
262
|
|
|
270
|
-
`recommended` turns all rules on at **error**.
|
|
271
|
-
|
|
272
263
|
| Rule | Forbids |
|
|
273
264
|
|------|---------|
|
|
274
265
|
| `prismakit/no-prisma-service-outside-repos` | Inject/reference `PrismaService` / `PrismaClient` outside allowlist |
|
|
@@ -276,23 +267,15 @@ export default [prismakit.configs.recommended];
|
|
|
276
267
|
| `prismakit/require-transaction-service` | `.$transaction` in feature code |
|
|
277
268
|
| `prismakit/require-cached-repo-provider` | Cached repo class missing from Nest `providers` |
|
|
278
269
|
|
|
279
|
-
Allowed path patterns
|
|
280
|
-
|
|
281
|
-
- `**/repositories/**`
|
|
282
|
-
- `**/infrastructure/prisma/**`
|
|
283
|
-
- `**/node_modules/**`
|
|
284
|
-
- `packages/(core|nestjs|redis)/**` (PrismaKit monorepo)
|
|
285
|
-
|
|
286
|
-
Mirror this layout rather than weakening the plugin.
|
|
270
|
+
Allowed path patterns: `**/repositories/**`, `**/infrastructure/prisma/**`.
|
|
287
271
|
|
|
288
272
|
## Bootstrap (plain Node)
|
|
289
273
|
|
|
290
274
|
```typescript
|
|
291
|
-
import {
|
|
275
|
+
import { PrismaClient } from '@prisma/client';
|
|
292
276
|
import {
|
|
293
277
|
createRepository,
|
|
294
278
|
loadPrismaMetaFromSchema,
|
|
295
|
-
setRegisteredCacheModels,
|
|
296
279
|
setComposeOptions,
|
|
297
280
|
setTelemetry,
|
|
298
281
|
RepositoryRegistry,
|
|
@@ -302,7 +285,6 @@ import { RedisCacheAdapter } from '@prismakit/redis';
|
|
|
302
285
|
|
|
303
286
|
const prisma = new PrismaClient();
|
|
304
287
|
loadPrismaMetaFromSchema('prisma/schema.prisma');
|
|
305
|
-
setRegisteredCacheModels(['user', 'post']);
|
|
306
288
|
setComposeOptions({ maxDepth: 6, parallel: true, setCache: true });
|
|
307
289
|
setTelemetry({ enabled: true, onEvent: (e) => console.debug('[pk]', e.type) });
|
|
308
290
|
|
|
@@ -312,7 +294,6 @@ const autoCompose = new AutoComposer(registry);
|
|
|
312
294
|
|
|
313
295
|
const UserRepo = createRepository({
|
|
314
296
|
model: 'user',
|
|
315
|
-
scalarFields: Prisma.UserScalarFieldEnum,
|
|
316
297
|
cache: { ttl: 86_400 },
|
|
317
298
|
});
|
|
318
299
|
const users = new UserRepo({ prisma, cache, registry, autoCompose });
|
|
@@ -26,8 +26,7 @@ Task Progress:
|
|
|
26
26
|
- [ ] New model access goes through `createRepository` (core) or the app's Nest `defineRepo` binder — not a one-off Prisma call
|
|
27
27
|
- [ ] File lives under `**/repositories/**` as `*.repository.ts`
|
|
28
28
|
- [ ] `model` is the Prisma client key (`'user'`, not `'User'`)
|
|
29
|
-
- [ ]
|
|
30
|
-
- [ ] `cacheModels` is omitted unless the app wants an extra allowlist
|
|
29
|
+
- [ ] Prisma meta is loaded at bootstrap (`dmmf` / `schemaPath` / `loadPrismaMetaFrom*`)
|
|
31
30
|
|
|
32
31
|
## Selects
|
|
33
32
|
|
|
@@ -54,7 +53,7 @@ Task Progress:
|
|
|
54
53
|
|
|
55
54
|
## Locks
|
|
56
55
|
|
|
57
|
-
- [ ] Repo declares `lock` (`true
|
|
56
|
+
- [ ] Repo declares `lock` (`true` or `{ tableName, columns }`) before any call passes `lock`
|
|
58
57
|
- [ ] Every `lock: { mode }` call also passes `tx`
|
|
59
58
|
- [ ] `skipLocked` is not combined with `nowait`
|
|
60
59
|
- [ ] Locked transactions stay short
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
name: prismakit-nestjs
|
|
3
3
|
description: >-
|
|
4
4
|
PrismaKit NestJS adapter: PrismaKitModule.forRoot/forRootAsync wiring, injectable repositories via
|
|
5
|
-
createDefineRepo/
|
|
6
|
-
and PRISMAKIT_* DI tokens. Use when working with @prismakit/nestjs,
|
|
7
|
-
Prisma data access, or Nest transactions and cache invalidation.
|
|
5
|
+
createDefineRepo (default) / createInjectableRepository (escape hatch), TransactionService.execTx
|
|
6
|
+
with afterCommit invalidation, and PRISMAKIT_* DI tokens. Use when working with @prismakit/nestjs,
|
|
7
|
+
Nest feature modules that need Prisma data access, or Nest transactions and cache invalidation.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# PrismaKit NestJS
|
|
11
11
|
|
|
12
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
13
|
|
|
14
|
+
**Line:** 4.0 (pre-stable). Default factory: **`createDefineRepo` / app `defineAppRepo`**.
|
|
15
|
+
|
|
14
16
|
## Bootstrap
|
|
15
17
|
|
|
16
18
|
```bash
|
|
@@ -23,7 +25,6 @@ pnpm add -D @prismakit/eslint-plugin @prismakit/cli
|
|
|
23
25
|
import { Module } from '@nestjs/common';
|
|
24
26
|
import { PrismaKitModule } from '@prismakit/nestjs';
|
|
25
27
|
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
26
|
-
import { Prisma } from '@prisma/client';
|
|
27
28
|
|
|
28
29
|
@Module({
|
|
29
30
|
imports: [
|
|
@@ -33,6 +34,11 @@ import { Prisma } from '@prisma/client';
|
|
|
33
34
|
schemaPath: 'prisma/schema.prisma', // default; Prisma 5/6: dmmf: Prisma.dmmf
|
|
34
35
|
validateCompose: true,
|
|
35
36
|
compose: { maxDepth: 6, parallel: true, setCache: true },
|
|
37
|
+
telemetry: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
slowThreshold: 500,
|
|
40
|
+
onEvent: (e) => console.debug(e.type),
|
|
41
|
+
},
|
|
36
42
|
}),
|
|
37
43
|
],
|
|
38
44
|
})
|
|
@@ -41,7 +47,7 @@ export class AppModule {}
|
|
|
41
47
|
|
|
42
48
|
`forRootAsync` when cache/URL come from `ConfigService` — see [examples.md](examples.md).
|
|
43
49
|
|
|
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
|
|
50
|
+
`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.
|
|
45
51
|
|
|
46
52
|
## Factory (one default)
|
|
47
53
|
|
|
@@ -96,14 +102,13 @@ export class AuditLogRepository extends defineAppRepo({
|
|
|
96
102
|
}) {}
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
Per-repo options: `model` (required), `cache?`, `lock?: true | RepositoryLockConfig`, `toPayload?`.
|
|
100
106
|
|
|
101
|
-
Escape
|
|
107
|
+
Escape hatch (do not use as the app default):
|
|
102
108
|
|
|
103
|
-
- `
|
|
104
|
-
- `createInjectableRepository` without a types bag — thin, results are `unknown`. Alias: `createPrismaRepository`.
|
|
109
|
+
- `createInjectableRepository` — thin Nest wrapper when TypeMap binding is unavailable (results are thinly typed).
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
Removed in 4.0: `defineInjectableRepository`, `defineRepo`, `defineRepository`, `createPrismaRepository`, Nest `cacheModels`, Nest `queryLog`.
|
|
107
112
|
|
|
108
113
|
## Register and inject
|
|
109
114
|
|
|
@@ -201,7 +206,7 @@ await this.tx.execTx(
|
|
|
201
206
|
|
|
202
207
|
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.
|
|
203
208
|
|
|
204
|
-
Repository `cache` is the source of truth.
|
|
209
|
+
Repository `cache` is the source of truth.
|
|
205
210
|
|
|
206
211
|
`cache.defaultSetCache: true` makes user-facing reads cache by default; still pass `setCache: false` on auth/uniqueness.
|
|
207
212
|
|
|
@@ -220,12 +225,10 @@ Repository `cache` is the source of truth. Omit `cacheModels` (fail-open). Pass
|
|
|
220
225
|
|
|
221
226
|
| Option | Use |
|
|
222
227
|
|--------|-----|
|
|
223
|
-
| `cacheModels` | Optional extra allowlist (omit — repo `cache` is enough) |
|
|
224
228
|
| `validateCompose: true` | Assert compose-safe selects on boot |
|
|
225
229
|
| `strictCachedRepos` | Fail boot if a `cache` repo class is not in Nest `providers` (default `true`) |
|
|
226
230
|
| `compose` | `{ maxDepth, parallel, setCache }` |
|
|
227
|
-
| `telemetry` | `{ enabled
|
|
228
|
-
| `queryLog` | `{ slowThreshold, onSlowQuery }` — enables telemetry / `query.slow` |
|
|
231
|
+
| `telemetry` | `{ enabled, slowThreshold, onSlowQuery, onEvent }` or `createPrismaKitTelemetry()` |
|
|
229
232
|
| `autoRegisterModels` | `true` or `string[]` — stub repos for compose-only models |
|
|
230
233
|
|
|
231
234
|
## Scaffolding
|
|
@@ -244,7 +247,7 @@ Reference app: [starter-prismakit-nestjs](https://github.com/fikiap23/starter-pr
|
|
|
244
247
|
|
|
245
248
|
## Clean code (Nest)
|
|
246
249
|
|
|
247
|
-
- One `
|
|
250
|
+
- One `defineAppRepo` binder under `src/infrastructure/prisma/`. Do not call `createDefineRepo` per feature.
|
|
248
251
|
- Feature modules own their repository providers; do not make every repo global.
|
|
249
252
|
- Controllers stay HTTP-only: map DTO → service method. No repository calls in controllers.
|
|
250
253
|
- Select presets live next to the repository (`minimal` / `general` / `withPassword`).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Production-shaped snippets. Contract: skill `prismakit` + [SKILL.md](SKILL.md).
|
|
4
4
|
|
|
5
|
-
## 1. App module (Redis,
|
|
5
|
+
## 1. App module (Redis, telemetry)
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
// src/app.module.ts
|
|
@@ -31,7 +31,8 @@ import { UserModule } from './modules/users/user.module';
|
|
|
31
31
|
validateCompose: true,
|
|
32
32
|
compose: { maxDepth: 6, parallel: true, setCache: true },
|
|
33
33
|
autoRegisterModels: true,
|
|
34
|
-
|
|
34
|
+
telemetry: {
|
|
35
|
+
enabled: true,
|
|
35
36
|
slowThreshold: 500,
|
|
36
37
|
onSlowQuery: (e) => {
|
|
37
38
|
console.warn(`Slow ${e.model}.${e.method}: ${e.durationMs}ms`);
|
|
@@ -47,24 +48,30 @@ export class AppModule {}
|
|
|
47
48
|
|
|
48
49
|
Prisma 5/6: pass `dmmf: Prisma.dmmf` instead of (or in addition to skipping) `schemaPath`. Prisma 7: `schemaPath` only.
|
|
49
50
|
|
|
50
|
-
## 2. TypeMap binder
|
|
51
|
+
## 2. TypeMap binder with app-wide cache defaults
|
|
51
52
|
|
|
52
53
|
```typescript
|
|
53
|
-
// src/infrastructure/prisma/define-repo.ts
|
|
54
|
+
// src/infrastructure/prisma/define-app-repo.ts
|
|
54
55
|
import { createDefineRepo } from '@prismakit/nestjs';
|
|
55
|
-
import type { Prisma } from '
|
|
56
|
+
import type { Prisma } from 'src/infrastructure/prisma/prisma-client';
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
const DAY = 86_400;
|
|
59
|
+
|
|
60
|
+
export const defineAppRepo = createDefineRepo<Prisma.TypeMap>({
|
|
61
|
+
cache: {
|
|
62
|
+
ttl: DAY,
|
|
63
|
+
nullTtl: 60,
|
|
64
|
+
defaultSetCache: true,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
58
67
|
```
|
|
59
68
|
|
|
60
69
|
## 3. Feature repository + select presets
|
|
61
70
|
|
|
62
71
|
```typescript
|
|
63
72
|
// src/modules/users/repositories/user.repository.ts
|
|
64
|
-
import { Prisma } from '
|
|
65
|
-
import {
|
|
66
|
-
|
|
67
|
-
const DAY = 86_400;
|
|
73
|
+
import { Prisma } from 'src/infrastructure/prisma/prisma-client';
|
|
74
|
+
import { defineAppRepo } from 'src/infrastructure/prisma/define-app-repo';
|
|
68
75
|
|
|
69
76
|
export const userSelectPresets = {
|
|
70
77
|
minimal: { id: true } satisfies Prisma.UserSelect,
|
|
@@ -80,18 +87,24 @@ export const userSelectPresets = {
|
|
|
80
87
|
} satisfies Prisma.UserSelect,
|
|
81
88
|
};
|
|
82
89
|
|
|
83
|
-
export
|
|
90
|
+
export class UserRepository extends defineAppRepo({
|
|
84
91
|
model: 'user',
|
|
85
|
-
scalarFields: Prisma.UserScalarFieldEnum,
|
|
86
92
|
cache: {
|
|
87
|
-
|
|
88
|
-
nullTtl: 60,
|
|
93
|
+
defaultSetCache: false, // auth lookups pass setCache explicitly
|
|
89
94
|
sensitiveFields: ['password'],
|
|
90
95
|
methods: { getFirst: { enabled: false } },
|
|
91
96
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
}) {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Simple cached repo — inherits app-wide defaults
|
|
102
|
+
import { defineAppRepo } from 'src/infrastructure/prisma/define-app-repo';
|
|
103
|
+
|
|
104
|
+
export class CategoryRepository extends defineAppRepo({
|
|
105
|
+
model: 'category',
|
|
106
|
+
cache: true,
|
|
107
|
+
}) {}
|
|
95
108
|
```
|
|
96
109
|
|
|
97
110
|
## 4. Feature module + thin controller + service
|