@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,235 @@
|
|
|
1
|
+
# PrismaKit NestJS examples
|
|
2
|
+
|
|
3
|
+
Production-shaped snippets. Contract: skill `prismakit` + [SKILL.md](SKILL.md).
|
|
4
|
+
|
|
5
|
+
## 1. App module (Redis, allowlist, slow-query log)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// src/app.module.ts
|
|
9
|
+
import { Module } from '@nestjs/common';
|
|
10
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
11
|
+
import { PrismaKitModule } from '@prismakit/nestjs';
|
|
12
|
+
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
13
|
+
import { PrismaClientModule } from './infrastructure/prisma/prisma-client.module';
|
|
14
|
+
import { PrismaService } from './infrastructure/prisma/prisma.service';
|
|
15
|
+
import { UserModule } from './modules/users/user.module';
|
|
16
|
+
|
|
17
|
+
@Module({
|
|
18
|
+
imports: [
|
|
19
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
20
|
+
PrismaClientModule,
|
|
21
|
+
PrismaKitModule.forRootAsync({
|
|
22
|
+
imports: [PrismaClientModule],
|
|
23
|
+
inject: [PrismaService, ConfigService],
|
|
24
|
+
useFactory: (prisma: PrismaService, config: ConfigService) => ({
|
|
25
|
+
prisma,
|
|
26
|
+
cache: new RedisCacheAdapter({
|
|
27
|
+
url: config.get<string>('REDIS_URL'),
|
|
28
|
+
prefix: config.get<string>('CACHE_PREFIX') ?? 'myapp',
|
|
29
|
+
}),
|
|
30
|
+
cacheModels: ['user', 'product', 'wallet'],
|
|
31
|
+
schemaPath: 'prisma/schema.prisma',
|
|
32
|
+
validateCompose: true,
|
|
33
|
+
compose: { maxDepth: 6, parallel: true, setCache: true },
|
|
34
|
+
autoRegisterModels: true,
|
|
35
|
+
queryLog: {
|
|
36
|
+
slowThreshold: 500,
|
|
37
|
+
onSlowQuery: (e) => {
|
|
38
|
+
console.warn(`Slow ${e.model}.${e.method}: ${e.durationMs}ms`);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
}),
|
|
43
|
+
UserModule,
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
export class AppModule {}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Prisma 5/6: pass `dmmf: Prisma.dmmf` instead of (or in addition to skipping) `schemaPath`. Prisma 7: `schemaPath` only.
|
|
50
|
+
|
|
51
|
+
## 2. TypeMap binder
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// src/infrastructure/prisma/define-repo.ts
|
|
55
|
+
import { createDefineRepo } from '@prismakit/nestjs';
|
|
56
|
+
import type { Prisma } from '@prisma/client';
|
|
57
|
+
|
|
58
|
+
export const defineRepo = createDefineRepo<Prisma.TypeMap>();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3. Feature repository + select presets
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// src/modules/users/repositories/user.repository.ts
|
|
65
|
+
import { Prisma } from '@prisma/client';
|
|
66
|
+
import { defineRepo } from '../../../infrastructure/prisma/define-repo';
|
|
67
|
+
|
|
68
|
+
const DAY = 86_400;
|
|
69
|
+
|
|
70
|
+
export const userSelectPresets = {
|
|
71
|
+
minimal: { id: true } satisfies Prisma.UserSelect,
|
|
72
|
+
general: {
|
|
73
|
+
id: true,
|
|
74
|
+
email: true,
|
|
75
|
+
name: true,
|
|
76
|
+
} satisfies Prisma.UserSelect,
|
|
77
|
+
withPassword: {
|
|
78
|
+
id: true,
|
|
79
|
+
email: true,
|
|
80
|
+
password: true,
|
|
81
|
+
} satisfies Prisma.UserSelect,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const UserRepository = defineRepo({
|
|
85
|
+
model: 'user',
|
|
86
|
+
scalarFields: Prisma.UserScalarFieldEnum,
|
|
87
|
+
cache: {
|
|
88
|
+
ttl: DAY,
|
|
89
|
+
nullTtl: 60,
|
|
90
|
+
sensitiveFields: ['password'],
|
|
91
|
+
methods: { getFirst: { enabled: false } },
|
|
92
|
+
},
|
|
93
|
+
lock: true,
|
|
94
|
+
});
|
|
95
|
+
export type UserRepository = InstanceType<typeof UserRepository>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 4. Feature module + thin controller + service
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// src/modules/users/user.module.ts
|
|
102
|
+
import { Module } from '@nestjs/common';
|
|
103
|
+
import { UserController } from './user.controller';
|
|
104
|
+
import { UserService } from './user.service';
|
|
105
|
+
import { UserRepository } from './repositories/user.repository';
|
|
106
|
+
|
|
107
|
+
@Module({
|
|
108
|
+
controllers: [UserController],
|
|
109
|
+
providers: [UserService, UserRepository],
|
|
110
|
+
exports: [UserService, UserRepository],
|
|
111
|
+
})
|
|
112
|
+
export class UserModule {}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// src/modules/users/user.controller.ts
|
|
117
|
+
import { Controller, Get, Param } from '@nestjs/common';
|
|
118
|
+
import { UserService } from './user.service';
|
|
119
|
+
|
|
120
|
+
@Controller('users')
|
|
121
|
+
export class UserController {
|
|
122
|
+
constructor(private readonly users: UserService) {}
|
|
123
|
+
|
|
124
|
+
@Get(':id')
|
|
125
|
+
getOne(@Param('id') id: string) {
|
|
126
|
+
return this.users.getProfile(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// src/modules/users/user.service.ts
|
|
133
|
+
import { Injectable } from '@nestjs/common';
|
|
134
|
+
import { UserRepository, userSelectPresets } from './repositories/user.repository';
|
|
135
|
+
|
|
136
|
+
@Injectable()
|
|
137
|
+
export class UserService {
|
|
138
|
+
constructor(private readonly users: UserRepository) {}
|
|
139
|
+
|
|
140
|
+
getProfile(id: string) {
|
|
141
|
+
return this.users.getThrowById({
|
|
142
|
+
id,
|
|
143
|
+
select: userSelectPresets.general,
|
|
144
|
+
setCache: true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async assertEmailFree(email: string) {
|
|
149
|
+
const hit = await this.users.getFirst({
|
|
150
|
+
where: { email },
|
|
151
|
+
select: userSelectPresets.minimal,
|
|
152
|
+
});
|
|
153
|
+
if (hit) throw new Error('email taken');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 5. Checkout — multi-repo `execTx`
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
@Injectable()
|
|
162
|
+
export class CheckoutService {
|
|
163
|
+
constructor(
|
|
164
|
+
private readonly tx: TransactionService,
|
|
165
|
+
private readonly orders: OrderRepository,
|
|
166
|
+
private readonly stocks: StockRepository,
|
|
167
|
+
) {}
|
|
168
|
+
|
|
169
|
+
handleCheckout(input: { stockId: string; qty: number; userId: string }) {
|
|
170
|
+
return this.tx.execTx(
|
|
171
|
+
async (tx) => {
|
|
172
|
+
const order = await this.orders.create({
|
|
173
|
+
tx,
|
|
174
|
+
data: { userId: input.userId, stockId: input.stockId, qty: input.qty },
|
|
175
|
+
select: { id: true },
|
|
176
|
+
invalidate: 'none',
|
|
177
|
+
});
|
|
178
|
+
await this.stocks.updateById({
|
|
179
|
+
tx,
|
|
180
|
+
id: input.stockId,
|
|
181
|
+
data: { qty: { decrement: input.qty } },
|
|
182
|
+
invalidate: 'none',
|
|
183
|
+
});
|
|
184
|
+
return order;
|
|
185
|
+
},
|
|
186
|
+
async () => {
|
|
187
|
+
await this.orders.invalidateCache({});
|
|
188
|
+
await this.stocks.invalidateCache({ id: input.stockId });
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Register `OrderRepository` and `StockRepository` in the feature module `providers`. Both need `cache` config for `invalidate: 'none'` / `invalidateCache` to exist on the type.
|
|
196
|
+
|
|
197
|
+
## 6. Wallet lock
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
@Injectable()
|
|
201
|
+
export class WalletService {
|
|
202
|
+
constructor(
|
|
203
|
+
private readonly tx: TransactionService,
|
|
204
|
+
private readonly wallets: WalletRepository,
|
|
205
|
+
) {}
|
|
206
|
+
|
|
207
|
+
debit(id: string, amount: number) {
|
|
208
|
+
return this.tx.execTx(
|
|
209
|
+
async (tx) => {
|
|
210
|
+
const wallet = await this.wallets.getById({
|
|
211
|
+
tx,
|
|
212
|
+
id,
|
|
213
|
+
select: { id: true, balance: true },
|
|
214
|
+
lock: { mode: 'update' },
|
|
215
|
+
});
|
|
216
|
+
if (!wallet || wallet.balance < amount) {
|
|
217
|
+
throw new Error('insufficient funds');
|
|
218
|
+
}
|
|
219
|
+
return this.wallets.updateById({
|
|
220
|
+
tx,
|
|
221
|
+
id,
|
|
222
|
+
data: { balance: wallet.balance - amount },
|
|
223
|
+
select: { id: true, balance: true },
|
|
224
|
+
invalidate: 'none',
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
async () => {
|
|
228
|
+
await this.wallets.invalidateCache({ id });
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`WalletRepository` must set `lock: true` (or a table/client key). Never call `lock` outside `execTx`.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# PrismaKit NestJS reference
|
|
2
|
+
|
|
3
|
+
API surface for `@prismakit/nestjs` 3.x. Repository methods, cache, compose, and locks are documented in skill `prismakit` (`reference.md` in that skill). This file covers the Nest adapter only.
|
|
4
|
+
|
|
5
|
+
## `PrismaKitModuleOptions`
|
|
6
|
+
|
|
7
|
+
| Option | Required | Description |
|
|
8
|
+
|--------|----------|-------------|
|
|
9
|
+
| `prisma` | yes | `PrismaClient` (or compatible). Provided as `PRISMAKIT_PRISMA` for repositories only. |
|
|
10
|
+
| `cache` | no | `CacheAdapter` (`RedisCacheAdapter` / `MemoryCacheAdapter`). |
|
|
11
|
+
| `dmmf` | no | `Prisma.dmmf` on Prisma 5/6. Skip on Prisma 7 — use `schemaPath`. |
|
|
12
|
+
| `schemaPath` | no | Load meta from `schema.prisma` when `dmmf` is omitted (compose + locks). Defaults to `prisma/schema.prisma`. |
|
|
13
|
+
| `validateCompose` | no | When `true`, `assertSelectComposeValid` on module init. |
|
|
14
|
+
| `cacheModels` | no | Strict allowlist of model keys with `cache` config. Omit = fail-open. |
|
|
15
|
+
| `compose` | no | `ComposeOptions`: `maxDepth` (default 10), `parallel` (default true), `setCache` (default true). `tx` is per-call only. |
|
|
16
|
+
| `telemetry` | no | `{ enabled?: boolean; onEvent?: (event) => void }`. |
|
|
17
|
+
| `queryLog` | no | `{ slowThreshold?: number; onSlowQuery?: (e) => void }`. Default threshold 500ms. Setting this enables telemetry. |
|
|
18
|
+
| `autoRegisterModels` | no | `true` = stub repos for all schema/DMMF models; `string[]` = those client keys only. |
|
|
19
|
+
|
|
20
|
+
`queryLog.onSlowQuery` receives `{ model?, method?, durationMs, thresholdMs }` for `query.complete` events at/above the threshold.
|
|
21
|
+
|
|
22
|
+
## Async config
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
export type PrismaKitModuleAsyncOptions = {
|
|
26
|
+
imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule>>;
|
|
27
|
+
useFactory: (...args: unknown[]) => Promise<PrismaKitModuleOptions> | PrismaKitModuleOptions;
|
|
28
|
+
inject?: unknown[];
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
PrismaKitModule.forRootAsync({
|
|
34
|
+
imports: [ConfigModule],
|
|
35
|
+
inject: [ConfigService],
|
|
36
|
+
useFactory: (config: ConfigService) => ({
|
|
37
|
+
prisma: prismaClient,
|
|
38
|
+
cache: new RedisCacheAdapter({
|
|
39
|
+
url: config.get('REDIS_URL'),
|
|
40
|
+
prefix: config.get('CACHE_PREFIX') ?? 'myapp',
|
|
41
|
+
}),
|
|
42
|
+
cacheModels: ['user', 'product'],
|
|
43
|
+
schemaPath: 'prisma/schema.prisma',
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## DI tokens
|
|
49
|
+
|
|
50
|
+
| Token | Type | Allowed injectors |
|
|
51
|
+
|-------|------|-------------------|
|
|
52
|
+
| `PRISMAKIT_PRISMA` | `PrismaClientLike` | Injectable repositories / kit internals only |
|
|
53
|
+
| `PRISMAKIT_CACHE` | `CacheAdapter` | Repositories / kit internals |
|
|
54
|
+
| `PRISMAKIT_OPTIONS` | `PrismaKitModuleOptions` | Kit internals |
|
|
55
|
+
|
|
56
|
+
`TransactionService`, `RepositoryRegistry`, and `AutoComposer` are Nest providers from `PrismaKitModule`.
|
|
57
|
+
|
|
58
|
+
## `TransactionService`
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
execTx<T, TClient = unknown>(
|
|
62
|
+
fn: (tx: TClient) => Promise<T>,
|
|
63
|
+
afterCommit?: () => Promise<void>,
|
|
64
|
+
options?: TransactionOptions,
|
|
65
|
+
): Promise<T>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`TransactionOptions`: `{ maxWait?: number; timeout?: number; isolationLevel?: 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable' | string }`.
|
|
69
|
+
|
|
70
|
+
Type the client when useful:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
await this.tx.execTx<User, Prisma.TransactionClient>(async (tx) => { /* ... */ });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`afterCommit` runs only after `prisma.$transaction` resolves successfully.
|
|
77
|
+
|
|
78
|
+
## Repository factories
|
|
79
|
+
|
|
80
|
+
| Factory | When |
|
|
81
|
+
|---------|------|
|
|
82
|
+
| `createDefineRepo<Prisma.TypeMap>()` then local `defineRepo({ model, ... })` | **Default** for apps. Zero phantoms; `model` + `scalarFields` + `cache` / `lock`. |
|
|
83
|
+
| `defineInjectableRepository({ model, select, create, update, where, orderBy, payload, ... })` | TypeMap unavailable. Package aliases: `defineRepository`. |
|
|
84
|
+
| `createInjectableRepository({ model, ... })` | Thin / untyped. Results `unknown` unless `toPayload` is supplied. Alias: `createPrismaRepository`. |
|
|
85
|
+
|
|
86
|
+
`createDefineRepo` runtime options: `model`, `scalarFields?`, `primaryKey?`, `cache?`, `lock?`, `schemaPath?`.
|
|
87
|
+
|
|
88
|
+
When `cache` is set, the returned API includes `setCache` / `cacheTags` / `invalidate` / `tags` / `invalidateCache`. Otherwise those fields are omitted from the type (`HasCacheFromOptions`).
|
|
89
|
+
|
|
90
|
+
`createDefineRepo` / `RepositoryApiFromTypeMap` includes the full runtime surface: `createMany`, `updateMany`, `upsert`, `deleteMany`, `lock` + `orderBy` on `getFirst`, `lock` on `getMany`, and composite-PK `id` on `*ById`. Derive the instance type from the constructor — do not restate model/cache as a second generic:
|
|
91
|
+
|
|
92
|
+
Export the instance type:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export type UserRepository = InstanceType<typeof UserRepository>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## `defineInjectableRepository` shape (escape hatch)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Prisma } from '@prisma/client';
|
|
102
|
+
import { defineInjectableRepository } from '@prismakit/nestjs';
|
|
103
|
+
|
|
104
|
+
type Of<S> = S extends Prisma.UserSelect
|
|
105
|
+
? Prisma.UserGetPayload<{ select: S }>
|
|
106
|
+
: never;
|
|
107
|
+
|
|
108
|
+
export const UserRepository = defineInjectableRepository({
|
|
109
|
+
model: 'user',
|
|
110
|
+
scalarFields: Prisma.UserScalarFieldEnum,
|
|
111
|
+
select: null! as Prisma.UserSelect,
|
|
112
|
+
create: null! as Prisma.UserCreateInput,
|
|
113
|
+
update: null! as Prisma.UserUpdateInput,
|
|
114
|
+
where: null! as Prisma.UserWhereInput,
|
|
115
|
+
orderBy: null! as Prisma.UserOrderByWithRelationInput,
|
|
116
|
+
payload: class {
|
|
117
|
+
declare readonly _select: unknown;
|
|
118
|
+
declare type: () => Of<this['_select']>;
|
|
119
|
+
},
|
|
120
|
+
cache: { ttl: 86_400, sensitiveFields: ['password'] },
|
|
121
|
+
lock: true,
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Re-exports from core
|
|
126
|
+
|
|
127
|
+
`@prismakit/nestjs` re-exports: `AutoComposer`, `RepositoryRegistry`, `CacheAdapter`, repository option/instance types, `RepoPayloadHKT`, `ComposeOptions`, `TelemetryOptions`, `TelemetryEvent`, `loadPrismaMetaFromDmmf`, `loadPrismaMetaFromSchema`, `setComposeOptions`, `setTelemetry`.
|
|
128
|
+
|
|
129
|
+
Prefer importing Nest-only APIs from `@prismakit/nestjs` and core-only helpers from `@prismakit/core`.
|
|
130
|
+
|
|
131
|
+
## Layout
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
src/
|
|
135
|
+
app.module.ts # PrismaKitModule.forRootAsync
|
|
136
|
+
infrastructure/prisma/
|
|
137
|
+
define-repo.ts # createDefineRepo<Prisma.TypeMap>()
|
|
138
|
+
prisma.service.ts # client construction only
|
|
139
|
+
modules/<feature>/
|
|
140
|
+
<feature>.module.ts # providers: [Service, XRepository]
|
|
141
|
+
<feature>.service.ts # inject repos + TransactionService
|
|
142
|
+
<feature>.controller.ts # HTTP only
|
|
143
|
+
repositories/<feature>.repository.ts
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Prisma client usage is allowed under `**/repositories/**` and `**/infrastructure/prisma/**` only.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# PrismaKit NestJS review checklist
|
|
2
|
+
|
|
3
|
+
Copy this list and tick every item before marking a Nest data-access change done. Also complete skill `prismakit` `review-checklist.md` (layering, cache, compose, locks).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Task Progress:
|
|
7
|
+
- [ ] Core contract (skill prismakit checklist)
|
|
8
|
+
- [ ] Module wiring
|
|
9
|
+
- [ ] Repository factory
|
|
10
|
+
- [ ] DI
|
|
11
|
+
- [ ] Transactions
|
|
12
|
+
- [ ] Feature shape
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Core contract
|
|
16
|
+
|
|
17
|
+
- [ ] Skill `prismakit` review checklist is complete for this change
|
|
18
|
+
|
|
19
|
+
## Module wiring
|
|
20
|
+
|
|
21
|
+
- [ ] `PrismaKitModule.forRoot` or `forRootAsync` is imported once at the app root
|
|
22
|
+
- [ ] `prisma` is the shared client instance
|
|
23
|
+
- [ ] Prisma meta is loaded: `dmmf: Prisma.dmmf` (Prisma 5/6) or `schemaPath: 'prisma/schema.prisma'` (Prisma 7)
|
|
24
|
+
- [ ] Production cache uses `RedisCacheAdapter` with a stable `prefix`
|
|
25
|
+
- [ ] `cacheModels` lists every model whose repository sets `cache`
|
|
26
|
+
- [ ] `validateCompose: true` is on for apps that nest relations in `select`
|
|
27
|
+
|
|
28
|
+
## Repository factory
|
|
29
|
+
|
|
30
|
+
- [ ] App has a single binder: `createDefineRepo<Prisma.TypeMap>()` in `src/infrastructure/prisma/define-repo.ts` (or equivalent)
|
|
31
|
+
- [ ] Feature repos call that binder — not `createInjectableRepository` unless types are intentionally thin
|
|
32
|
+
- [ ] `export type XRepository = InstanceType<typeof XRepository>`
|
|
33
|
+
- [ ] Repo class is in `providers` and `exports` of the feature module
|
|
34
|
+
- [ ] `lock: true` (or table/client key) is set when any call uses `lock`
|
|
35
|
+
|
|
36
|
+
## DI
|
|
37
|
+
|
|
38
|
+
- [ ] Services inject `*Repository` and `TransactionService` — never `PRISMAKIT_PRISMA`, `PrismaClient`, or `PrismaService`
|
|
39
|
+
- [ ] Controllers inject services — not repositories, not Prisma
|
|
40
|
+
- [ ] `PRISMAKIT_PRISMA` appears only inside `**/repositories/**` or kit internals
|
|
41
|
+
|
|
42
|
+
## Transactions
|
|
43
|
+
|
|
44
|
+
- [ ] Multi-step writes use `this.tx.execTx(fn, afterCommit)`
|
|
45
|
+
- [ ] No `prisma.$transaction` / `.$transaction` in feature code
|
|
46
|
+
- [ ] Every repo call inside `fn` receives `tx`
|
|
47
|
+
- [ ] Writes use `invalidate: 'none'`
|
|
48
|
+
- [ ] Matching `invalidateCache` calls live in `afterCommit`
|
|
49
|
+
- [ ] Row locks run only inside `execTx`
|
|
50
|
+
|
|
51
|
+
## Feature shape
|
|
52
|
+
|
|
53
|
+
- [ ] HTTP mapping stays in the controller; orchestration in the service; Prisma in the repository
|
|
54
|
+
- [ ] Select presets live next to the repository (`minimal` / `general` / `withPassword`)
|
|
55
|
+
- [ ] After `prismakit generate --full`, the new `*Module` is imported in `AppModule`
|
|
56
|
+
- [ ] After repo-only generate, the class is registered in feature `providers`
|
|
57
|
+
- [ ] ESLint `prismakit.configs.recommended` passes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { findSkillsRoot, runSkills } from '../commands/skills';
|
|
8
|
+
|
|
9
|
+
const repoSkills = path.resolve(
|
|
10
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
11
|
+
'../../../../skills',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const tmpDirs: string[] = [];
|
|
15
|
+
|
|
16
|
+
function tmpDir(): string {
|
|
17
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'prismakit-skills-'));
|
|
18
|
+
tmpDirs.push(dir);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
for (const dir of tmpDirs.splice(0)) {
|
|
24
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('findSkillsRoot', () => {
|
|
29
|
+
it('finds bundled or monorepo skills/ from this test file', () => {
|
|
30
|
+
const found = findSkillsRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
31
|
+
expect(found).toBeTruthy();
|
|
32
|
+
expect(fs.existsSync(path.join(found!, 'prismakit', 'SKILL.md'))).toBe(
|
|
33
|
+
true,
|
|
34
|
+
);
|
|
35
|
+
expect(
|
|
36
|
+
fs.existsSync(path.join(found!, 'prismakit-nestjs', 'SKILL.md')),
|
|
37
|
+
).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('runSkills', () => {
|
|
42
|
+
it('lists bundled skills', () => {
|
|
43
|
+
const result = runSkills({ list: true, skillsRoot: repoSkills });
|
|
44
|
+
expect(result.installed).toEqual([]);
|
|
45
|
+
expect(result.dest).toBe(repoSkills);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('installs both skills into a project .cursor/skills', () => {
|
|
49
|
+
const cwd = tmpDir();
|
|
50
|
+
const result = runSkills({ cwd, skillsRoot: repoSkills });
|
|
51
|
+
expect(result.installed).toEqual(['prismakit', 'prismakit-nestjs']);
|
|
52
|
+
expect(
|
|
53
|
+
fs.existsSync(path.join(cwd, '.cursor/skills/prismakit/SKILL.md')),
|
|
54
|
+
).toBe(true);
|
|
55
|
+
expect(
|
|
56
|
+
fs.existsSync(
|
|
57
|
+
path.join(cwd, '.cursor/skills/prismakit-nestjs/SKILL.md'),
|
|
58
|
+
),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
expect(
|
|
61
|
+
fs.readFileSync(
|
|
62
|
+
path.join(cwd, '.cursor/skills/prismakit/SKILL.md'),
|
|
63
|
+
'utf-8',
|
|
64
|
+
),
|
|
65
|
+
).toContain('name: prismakit');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('installs a single skill and overwrites on re-run', () => {
|
|
69
|
+
const cwd = tmpDir();
|
|
70
|
+
runSkills({ cwd, skillsRoot: repoSkills, skill: ['prismakit'] });
|
|
71
|
+
const skillFile = path.join(cwd, '.cursor/skills/prismakit/SKILL.md');
|
|
72
|
+
fs.appendFileSync(skillFile, '\n# dirty\n');
|
|
73
|
+
runSkills({ cwd, skillsRoot: repoSkills, skill: ['prismakit'] });
|
|
74
|
+
expect(fs.readFileSync(skillFile, 'utf-8')).not.toContain('# dirty');
|
|
75
|
+
expect(
|
|
76
|
+
fs.existsSync(path.join(cwd, '.cursor/skills/prismakit-nestjs')),
|
|
77
|
+
).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('dry-run does not write files', () => {
|
|
81
|
+
const cwd = tmpDir();
|
|
82
|
+
runSkills({ cwd, skillsRoot: repoSkills, dryRun: true });
|
|
83
|
+
expect(fs.existsSync(path.join(cwd, '.cursor'))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('copies the data-access rule with --with-rules', () => {
|
|
87
|
+
const cwd = tmpDir();
|
|
88
|
+
const rulesPath = path.resolve(
|
|
89
|
+
repoSkills,
|
|
90
|
+
'../templates/cursor-rules/data-access.mdc',
|
|
91
|
+
);
|
|
92
|
+
const result = runSkills({
|
|
93
|
+
cwd,
|
|
94
|
+
skillsRoot: repoSkills,
|
|
95
|
+
withRules: true,
|
|
96
|
+
rulesPath,
|
|
97
|
+
skill: ['prismakit'],
|
|
98
|
+
});
|
|
99
|
+
expect(result.ruleDest).toBe(
|
|
100
|
+
path.join(cwd, '.cursor/rules/data-access.mdc'),
|
|
101
|
+
);
|
|
102
|
+
expect(fs.existsSync(result.ruleDest!)).toBe(true);
|
|
103
|
+
expect(fs.readFileSync(result.ruleDest!, 'utf-8')).toContain(
|
|
104
|
+
'repository-only',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('rejects unknown skill names', () => {
|
|
109
|
+
expect(() =>
|
|
110
|
+
runSkills({
|
|
111
|
+
cwd: tmpDir(),
|
|
112
|
+
skillsRoot: repoSkills,
|
|
113
|
+
skill: ['nope'],
|
|
114
|
+
}),
|
|
115
|
+
).toThrow(/Unknown skill/);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/bin.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { runGenerate } from './commands/generate';
|
|
2
|
-
import { runCodegen } from './commands/codegen';
|
|
3
2
|
import { runValidate } from './commands/validate';
|
|
3
|
+
import { runSkills } from './commands/skills';
|
|
4
4
|
|
|
5
5
|
function printHelp(): void {
|
|
6
6
|
console.log(`prismakit — PrismaKit CLI
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
prismakit generate <name> [--cache] [--full] [--helpers] [--dto] [--route <path>] [--prisma-import <path>] [--dry-run]
|
|
10
|
-
prismakit
|
|
11
|
-
prismakit
|
|
10
|
+
prismakit validate [--schema <path>] [--auto-register] [--no-assert]
|
|
11
|
+
prismakit skills [--global] [--with-rules] [--skill <name>] [--project <path>] [--list] [--dry-run]
|
|
12
12
|
prismakit help
|
|
13
13
|
|
|
14
14
|
By default, generate writes only the repository file.
|
|
@@ -18,8 +18,10 @@ Pass --helpers / --dto with --full for helpers and Swagger DTOs.
|
|
|
18
18
|
Examples:
|
|
19
19
|
prismakit generate product --cache
|
|
20
20
|
prismakit generate product --cache --full --helpers --dto --route products
|
|
21
|
-
prismakit
|
|
22
|
-
prismakit
|
|
21
|
+
prismakit validate --auto-register
|
|
22
|
+
prismakit skills
|
|
23
|
+
prismakit skills --global
|
|
24
|
+
prismakit skills --with-rules
|
|
23
25
|
`);
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -34,10 +36,12 @@ function parseArgs(argv: string[]): {
|
|
|
34
36
|
|
|
35
37
|
for (let i = 0; i < rest.length; i++) {
|
|
36
38
|
const arg = rest[i];
|
|
37
|
-
if (arg
|
|
39
|
+
if (arg === '-g') {
|
|
40
|
+
flags.global = true;
|
|
41
|
+
} else if (arg.startsWith('--')) {
|
|
38
42
|
const key = arg.slice(2);
|
|
39
43
|
const next = rest[i + 1];
|
|
40
|
-
if (next && !next.startsWith('
|
|
44
|
+
if (next && !next.startsWith('-')) {
|
|
41
45
|
flags[key] = next;
|
|
42
46
|
i++;
|
|
43
47
|
} else {
|
|
@@ -80,18 +84,29 @@ function main(): void {
|
|
|
80
84
|
});
|
|
81
85
|
break;
|
|
82
86
|
}
|
|
83
|
-
case '
|
|
84
|
-
|
|
87
|
+
case 'validate': {
|
|
88
|
+
runValidate({
|
|
89
|
+
assert: !flags['no-assert'],
|
|
85
90
|
schemaPath:
|
|
86
91
|
typeof flags.schema === 'string' ? flags.schema : undefined,
|
|
87
|
-
|
|
88
|
-
outFile: typeof flags.out === 'string' ? flags.out : undefined,
|
|
92
|
+
autoRegisterModels: !!flags['auto-register'],
|
|
89
93
|
});
|
|
90
94
|
break;
|
|
91
95
|
}
|
|
92
|
-
case '
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
case 'skills':
|
|
97
|
+
case 'skill': {
|
|
98
|
+
const skillFlag = flags.skill;
|
|
99
|
+
runSkills({
|
|
100
|
+
global: !!flags.global,
|
|
101
|
+
withRules: !!flags['with-rules'],
|
|
102
|
+
dryRun: !!flags['dry-run'],
|
|
103
|
+
list: !!flags.list,
|
|
104
|
+
projectRoot:
|
|
105
|
+
typeof flags.project === 'string' ? flags.project : undefined,
|
|
106
|
+
skill:
|
|
107
|
+
typeof skillFlag === 'string'
|
|
108
|
+
? skillFlag.split(',').map((s) => s.trim()).filter(Boolean)
|
|
109
|
+
: undefined,
|
|
95
110
|
});
|
|
96
111
|
break;
|
|
97
112
|
}
|