@prismakit/cli 2.1.0 → 2.2.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.
@@ -0,0 +1,69 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ import {
5
+ computeRelationAliasesFromSchema,
6
+ getSchemaModels,
7
+ } from '@prismakit/core';
8
+
9
+ export type CodegenCommandOptions = {
10
+ cwd?: string;
11
+ schemaPath?: string;
12
+ write?: boolean;
13
+ outFile?: string;
14
+ };
15
+
16
+ /**
17
+ * Parse prisma/schema.prisma and print (or write) suggested relation aliases.
18
+ */
19
+ export function runCodegen(options: CodegenCommandOptions = {}): void {
20
+ const cwd = options.cwd ?? process.cwd();
21
+ const schemaPath =
22
+ options.schemaPath ?? path.join(cwd, 'prisma', 'schema.prisma');
23
+
24
+ if (!fs.existsSync(schemaPath)) {
25
+ throw new Error(`Prisma schema not found at ${schemaPath}`);
26
+ }
27
+
28
+ const models = getSchemaModels(schemaPath);
29
+ const aliases = computeRelationAliasesFromSchema(models);
30
+
31
+ const entries = Object.entries(aliases).sort(([a], [b]) =>
32
+ a.localeCompare(b),
33
+ );
34
+
35
+ if (entries.length === 0) {
36
+ console.log(
37
+ 'No additional relation aliases suggested (suffix rules cover all).',
38
+ );
39
+ return;
40
+ }
41
+
42
+ const lines = [
43
+ '// Suggested RELATION_MODEL_ALIASES entries (merge into your resolver config)',
44
+ 'export const SUGGESTED_RELATION_MODEL_ALIASES = {',
45
+ ...entries.map(([k, v]) => ` ${k}: '${v}',`),
46
+ '} as const;',
47
+ '',
48
+ ];
49
+ const output = lines.join('\n');
50
+
51
+ if (options.write) {
52
+ const out =
53
+ options.outFile ??
54
+ path.join(
55
+ cwd,
56
+ 'src',
57
+ 'infrastructure',
58
+ 'prisma',
59
+ 'suggested-relation-aliases.ts',
60
+ );
61
+ fs.mkdirSync(path.dirname(out), { recursive: true });
62
+ fs.writeFileSync(out, output, 'utf-8');
63
+ console.log(`wrote ${path.relative(cwd, out)}`);
64
+ } else {
65
+ console.log(output);
66
+ }
67
+
68
+ console.log(`\n${entries.length} alias suggestion(s).`);
69
+ }
@@ -0,0 +1,63 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ import { resolveNames } from '../naming';
5
+ import { renderModuleFiles } from '../templates';
6
+
7
+ export type GenerateCommandOptions = {
8
+ name: string;
9
+ cache?: boolean;
10
+ route?: string;
11
+ cwd?: string;
12
+ dryRun?: boolean;
13
+ /** Emit full Nest module (controller/service/types). Default: repo-only. */
14
+ full?: boolean;
15
+ /** Emit validate + mapper helpers. */
16
+ helpers?: boolean;
17
+ /** Emit class-validator DTOs. */
18
+ dto?: boolean;
19
+ /** Prisma client import path (default `@prisma/client`). */
20
+ prismaImport?: string;
21
+ };
22
+
23
+ export function runGenerate(options: GenerateCommandOptions): void {
24
+ const cwd = options.cwd ?? process.cwd();
25
+ const names = resolveNames(options.name, options.route);
26
+ const full = !!options.full;
27
+ const files = renderModuleFiles({
28
+ names,
29
+ cacheEnabled: !!options.cache,
30
+ full,
31
+ helpers: !!options.helpers,
32
+ dto: !!options.dto,
33
+ prismaImport: options.prismaImport,
34
+ });
35
+
36
+ for (const file of files) {
37
+ const fullPath = path.join(cwd, file.relativePath);
38
+ if (options.dryRun) {
39
+ console.log(`[dry-run] would write ${file.relativePath}`);
40
+ continue;
41
+ }
42
+ if (fs.existsSync(fullPath)) {
43
+ console.warn(`skip (exists): ${file.relativePath}`);
44
+ continue;
45
+ }
46
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
47
+ const content = file.content.endsWith('\n')
48
+ ? file.content
49
+ : `${file.content}\n`;
50
+ fs.writeFileSync(fullPath, content, 'utf-8');
51
+ console.log(`created ${file.relativePath}`);
52
+ }
53
+
54
+ if (full) {
55
+ console.log(
56
+ `\nScaffolded module "${names.kebab}". Register ${names.pascal}Module in app.module.ts.`,
57
+ );
58
+ } else {
59
+ console.log(
60
+ `\nScaffolded repository "${names.pascal}Repository". Register it in your feature module providers.`,
61
+ );
62
+ }
63
+ }
@@ -0,0 +1,38 @@
1
+ import {
2
+ assertSelectComposeValid,
3
+ validateSelectCompose,
4
+ } from '@prismakit/core';
5
+
6
+ export type ValidateCommandOptions = {
7
+ cwd?: string;
8
+ assert?: boolean;
9
+ };
10
+
11
+ /**
12
+ * Run select-compose validation from @prismakit/core.
13
+ */
14
+ export function runValidate(options: ValidateCommandOptions = {}): void {
15
+ const cwd = options.cwd ?? process.cwd();
16
+
17
+ if (options.assert !== false) {
18
+ try {
19
+ assertSelectComposeValid(cwd);
20
+ console.log('Select compose validation passed.');
21
+ } catch (err) {
22
+ console.error((err as Error).message);
23
+ process.exitCode = 1;
24
+ }
25
+ return;
26
+ }
27
+
28
+ const issues = validateSelectCompose(cwd);
29
+ if (issues.length === 0) {
30
+ console.log('Select compose validation passed.');
31
+ return;
32
+ }
33
+
34
+ for (const issue of issues) {
35
+ console.error(` - ${issue.file}: ${issue.message}`);
36
+ }
37
+ process.exitCode = 1;
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { runGenerate, type GenerateCommandOptions } from './commands/generate';
2
+ export { runCodegen, type CodegenCommandOptions } from './commands/codegen';
3
+ export { runValidate, type ValidateCommandOptions } from './commands/validate';
4
+ export { resolveNames, assertKebabName, type ModuleNames } from './naming';
5
+ export { renderModuleFiles, type GeneratedFile, type GenerateOptions } from './templates';
package/src/naming.ts ADDED
@@ -0,0 +1,42 @@
1
+ export interface ModuleNames {
2
+ kebab: string;
3
+ camel: string;
4
+ pascal: string;
5
+ repoModel: string;
6
+ route: string;
7
+ }
8
+
9
+ const KEBAB_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
10
+
11
+ export function assertKebabName(name: string): void {
12
+ if (!KEBAB_NAME_RE.test(name)) {
13
+ throw new Error(
14
+ `Invalid module name "${name}". Use kebab-case (e.g. product, blog-post).`,
15
+ );
16
+ }
17
+ }
18
+
19
+ function kebabToPascal(kebab: string): string {
20
+ return kebab
21
+ .split('-')
22
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join('');
24
+ }
25
+
26
+ function kebabToCamel(kebab: string): string {
27
+ const pascal = kebabToPascal(kebab);
28
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
29
+ }
30
+
31
+ export function resolveNames(kebab: string, route?: string): ModuleNames {
32
+ assertKebabName(kebab);
33
+ const pascal = kebabToPascal(kebab);
34
+ const camel = kebabToCamel(kebab);
35
+ return {
36
+ kebab,
37
+ camel,
38
+ pascal,
39
+ repoModel: camel,
40
+ route: route ?? kebab,
41
+ };
42
+ }
@@ -0,0 +1,423 @@
1
+ import type { ModuleNames } from './naming';
2
+
3
+ export type GenerateOptions = {
4
+ names: ModuleNames;
5
+ cacheEnabled: boolean;
6
+ /** When false (default), only emit the repository file. */
7
+ full?: boolean;
8
+ /** Emit validate + mapper helpers. */
9
+ helpers?: boolean;
10
+ /** Emit class-validator DTOs with @ApiProperty. */
11
+ dto?: boolean;
12
+ /** Prisma client import path (default `@prisma/client`). */
13
+ prismaImport?: string;
14
+ };
15
+
16
+ function apply(template: string, names: ModuleNames, extras: Record<string, string>): string {
17
+ const replacements: Record<string, string> = {
18
+ '{{pascal}}': names.pascal,
19
+ '{{camel}}': names.camel,
20
+ '{{kebab}}': names.kebab,
21
+ '{{route}}': names.route,
22
+ '{{repoModel}}': names.repoModel,
23
+ ...extras,
24
+ };
25
+ let result = template;
26
+ for (const [key, value] of Object.entries(replacements)) {
27
+ result = result.split(key).join(value);
28
+ }
29
+ return result;
30
+ }
31
+
32
+ export type GeneratedFile = {
33
+ relativePath: string;
34
+ content: string;
35
+ };
36
+
37
+ function renderRepository(
38
+ names: ModuleNames,
39
+ cacheEnabled: boolean,
40
+ prismaImport: string,
41
+ base: string,
42
+ ): GeneratedFile {
43
+ const cacheBlock = cacheEnabled
44
+ ? ` cache: {
45
+ ttl: 86400,
46
+ sensitiveFields: ['password'],
47
+ defaultSetCache: true,
48
+ },
49
+ `
50
+ : '';
51
+
52
+ const content = apply(
53
+ `import { Prisma } from '{{prismaImport}}';
54
+ import { createInjectableRepository } from '@prismakit/nestjs';
55
+
56
+ export const {{pascal}}Repository = createInjectableRepository({
57
+ model: '{{repoModel}}',
58
+ scalarFields: Prisma.{{pascal}}ScalarFieldEnum,
59
+ {{cacheBlock}}});
60
+
61
+ export type {{pascal}}Repository = InstanceType<typeof {{pascal}}Repository>;
62
+ `,
63
+ names,
64
+ {
65
+ '{{cacheBlock}}': cacheBlock,
66
+ '{{prismaImport}}': prismaImport,
67
+ },
68
+ );
69
+
70
+ return {
71
+ relativePath: `${base}/repositories/${names.kebab}.repository.ts`,
72
+ content,
73
+ };
74
+ }
75
+
76
+ export function renderModuleFiles(options: GenerateOptions): GeneratedFile[] {
77
+ const { names, cacheEnabled, full = false, helpers = false, dto = false } =
78
+ options;
79
+ const prismaImport = options.prismaImport ?? '@prisma/client';
80
+ const base = `src/modules/${names.kebab}`;
81
+
82
+ const repository = renderRepository(names, cacheEnabled, prismaImport, base);
83
+
84
+ if (!full) {
85
+ return [repository];
86
+ }
87
+
88
+ const service = apply(
89
+ `import { Injectable } from '@nestjs/common';
90
+
91
+ import { {{pascal}}Repository } from '../repositories/{{kebab}}.repository';
92
+ import { get{{pascal}}Select } from '../types/select-{{kebab}}.type';
93
+ import { where{{pascal}}GetManyPaginate } from '../types/where-{{kebab}}.type';
94
+ {{dtoImport}}
95
+ @Injectable()
96
+ export class {{pascal}}Service {
97
+ constructor(private readonly {{camel}}Repository: {{pascal}}Repository) {}
98
+
99
+ async handleCreate(dto: Create{{pascal}}Dto) {
100
+ return await this.{{camel}}Repository.create({
101
+ data: { ...dto },
102
+ select: get{{pascal}}Select('general'),
103
+ });
104
+ }
105
+
106
+ async handleGetById(id: string) {
107
+ return await this.{{camel}}Repository.getThrowById({
108
+ id,
109
+ select: get{{pascal}}Select('general'),
110
+ setCache: true,
111
+ });
112
+ }
113
+
114
+ async handleGetManyPaginate(filter: Filter{{pascal}}Dto) {
115
+ const { where } = where{{pascal}}GetManyPaginate(filter);
116
+ return await this.{{camel}}Repository.getManyPaginate({
117
+ where,
118
+ select: get{{pascal}}Select('general'),
119
+ page: filter.page,
120
+ pageSize: filter.pageSize,
121
+ setCache: true,
122
+ });
123
+ }
124
+
125
+ async handleUpdateById(id: string, dto: Update{{pascal}}Dto) {
126
+ return await this.{{camel}}Repository.updateById({
127
+ id,
128
+ data: { ...dto },
129
+ select: get{{pascal}}Select('general'),
130
+ });
131
+ }
132
+
133
+ async handleDeleteById(id: string) {
134
+ return await this.{{camel}}Repository.deleteById({
135
+ id,
136
+ select: get{{pascal}}Select('minimal'),
137
+ });
138
+ }
139
+ }
140
+ `,
141
+ names,
142
+ {
143
+ '{{dtoImport}}': dto
144
+ ? `import type {\n Create{{pascal}}Dto,\n Update{{pascal}}Dto,\n Filter{{pascal}}Dto,\n} from '../dto/{{kebab}}.dto';\n`
145
+ : `type Create{{pascal}}Dto = Record<string, unknown>;\ntype Update{{pascal}}Dto = Record<string, unknown>;\ntype Filter{{pascal}}Dto = { page?: number; pageSize?: number; q?: string };\n`,
146
+ },
147
+ );
148
+
149
+ const controller = apply(
150
+ `import {
151
+ Body,
152
+ Controller,
153
+ Delete,
154
+ Get,
155
+ HttpStatus,
156
+ Param,
157
+ Patch,
158
+ Post,
159
+ Query,
160
+ Res,
161
+ } from '@nestjs/common';
162
+ import type { Response } from 'express';
163
+
164
+ import { {{pascal}}Service } from '../services/{{kebab}}.service';
165
+ {{dtoImport}}
166
+ @Controller('{{route}}')
167
+ export class {{pascal}}Controller {
168
+ constructor(private readonly {{camel}}Service: {{pascal}}Service) {}
169
+
170
+ @Post()
171
+ async create(@Body() dto: Create{{pascal}}Dto, @Res() res: Response) {
172
+ try {
173
+ const result = await this.{{camel}}Service.handleCreate(dto);
174
+ return res.status(HttpStatus.CREATED).json({ data: result });
175
+ } catch (error) {
176
+ const status = (error as { statusCode?: number })?.statusCode ?? 500;
177
+ return res.status(status).json({
178
+ error: { message: (error as Error).message, httpStatus: status },
179
+ });
180
+ }
181
+ }
182
+
183
+ @Get()
184
+ async getMany(@Query() filter: Filter{{pascal}}Dto, @Res() res: Response) {
185
+ try {
186
+ const result = await this.{{camel}}Service.handleGetManyPaginate(filter);
187
+ return res.status(HttpStatus.OK).json({ data: result.data, meta: result.meta });
188
+ } catch (error) {
189
+ const status = (error as { statusCode?: number })?.statusCode ?? 500;
190
+ return res.status(status).json({
191
+ error: { message: (error as Error).message, httpStatus: status },
192
+ });
193
+ }
194
+ }
195
+
196
+ @Get(':id')
197
+ async getById(@Param('id') id: string, @Res() res: Response) {
198
+ try {
199
+ const result = await this.{{camel}}Service.handleGetById(id);
200
+ return res.status(HttpStatus.OK).json({ data: result });
201
+ } catch (error) {
202
+ const status = (error as { statusCode?: number })?.statusCode ?? 500;
203
+ return res.status(status).json({
204
+ error: { message: (error as Error).message, httpStatus: status },
205
+ });
206
+ }
207
+ }
208
+
209
+ @Patch(':id')
210
+ async update(
211
+ @Param('id') id: string,
212
+ @Body() dto: Update{{pascal}}Dto,
213
+ @Res() res: Response,
214
+ ) {
215
+ try {
216
+ const result = await this.{{camel}}Service.handleUpdateById(id, dto);
217
+ return res.status(HttpStatus.OK).json({ data: result });
218
+ } catch (error) {
219
+ const status = (error as { statusCode?: number })?.statusCode ?? 500;
220
+ return res.status(status).json({
221
+ error: { message: (error as Error).message, httpStatus: status },
222
+ });
223
+ }
224
+ }
225
+
226
+ @Delete(':id')
227
+ async delete(@Param('id') id: string, @Res() res: Response) {
228
+ try {
229
+ const result = await this.{{camel}}Service.handleDeleteById(id);
230
+ return res.status(HttpStatus.OK).json({ data: result });
231
+ } catch (error) {
232
+ const status = (error as { statusCode?: number })?.statusCode ?? 500;
233
+ return res.status(status).json({
234
+ error: { message: (error as Error).message, httpStatus: status },
235
+ });
236
+ }
237
+ }
238
+ }
239
+ `,
240
+ names,
241
+ {
242
+ '{{dtoImport}}': dto
243
+ ? `import type {\n Create{{pascal}}Dto,\n Update{{pascal}}Dto,\n Filter{{pascal}}Dto,\n} from '../dto/{{kebab}}.dto';\n`
244
+ : `type Create{{pascal}}Dto = Record<string, unknown>;\ntype Update{{pascal}}Dto = Record<string, unknown>;\ntype Filter{{pascal}}Dto = { page?: number; pageSize?: number; q?: string };\n`,
245
+ },
246
+ );
247
+
248
+ const moduleFile = apply(
249
+ `import { Module } from '@nestjs/common';
250
+
251
+ import { {{pascal}}Controller } from './controllers/{{kebab}}.controller';
252
+ import { {{pascal}}Service } from './services/{{kebab}}.service';
253
+ import { {{pascal}}Repository } from './repositories/{{kebab}}.repository';
254
+ {{helpersImport}}
255
+ @Module({
256
+ controllers: [{{pascal}}Controller],
257
+ providers: [{{pascal}}Service, {{pascal}}Repository{{helpersProviders}}],
258
+ exports: [{{pascal}}Service, {{pascal}}Repository],
259
+ })
260
+ export class {{pascal}}Module {}
261
+ `,
262
+ names,
263
+ {
264
+ '{{helpersImport}}': helpers
265
+ ? `import { {{pascal}}ValidateHelper } from './helpers/{{kebab}}-validate.helper';\nimport { {{pascal}}MapperHelper } from './helpers/{{kebab}}-mapper.helper';\n`
266
+ : '',
267
+ '{{helpersProviders}}': helpers
268
+ ? `, {{pascal}}ValidateHelper, {{pascal}}MapperHelper`
269
+ : '',
270
+ },
271
+ );
272
+
273
+ const select = apply(
274
+ `import { Prisma } from '{{prismaImport}}';
275
+
276
+ type {{pascal}}SelectPresetKey = keyof typeof {{camel}}SelectPresets;
277
+
278
+ export function get{{pascal}}Select<K extends {{pascal}}SelectPresetKey>(key: K) {
279
+ return {{camel}}SelectPresets[key];
280
+ }
281
+
282
+ export const {{camel}}SelectPresets = {
283
+ minimal: {
284
+ id: true,
285
+ } satisfies Prisma.{{pascal}}Select,
286
+
287
+ general: {
288
+ id: true,
289
+ } satisfies Prisma.{{pascal}}Select,
290
+ };
291
+ `,
292
+ names,
293
+ { '{{prismaImport}}': prismaImport },
294
+ );
295
+
296
+ const where = apply(
297
+ `import { Prisma } from '{{prismaImport}}';
298
+
299
+ export function where{{pascal}}GetManyPaginate(filter: {
300
+ q?: string;
301
+ }): {
302
+ where: Prisma.{{pascal}}WhereInput;
303
+ } {
304
+ const { q } = filter;
305
+ const where: Prisma.{{pascal}}WhereInput = {
306
+ ...(q ? { /* add searchable fields */ } : {}),
307
+ };
308
+ return { where };
309
+ }
310
+ `,
311
+ names,
312
+ { '{{prismaImport}}': prismaImport },
313
+ );
314
+
315
+ const files: GeneratedFile[] = [
316
+ { relativePath: `${base}/${names.kebab}.module.ts`, content: moduleFile },
317
+ {
318
+ relativePath: `${base}/controllers/${names.kebab}.controller.ts`,
319
+ content: controller,
320
+ },
321
+ {
322
+ relativePath: `${base}/services/${names.kebab}.service.ts`,
323
+ content: service,
324
+ },
325
+ repository,
326
+ {
327
+ relativePath: `${base}/types/select-${names.kebab}.type.ts`,
328
+ content: select,
329
+ },
330
+ {
331
+ relativePath: `${base}/types/where-${names.kebab}.type.ts`,
332
+ content: where,
333
+ },
334
+ ];
335
+
336
+ if (dto) {
337
+ files.push({
338
+ relativePath: `${base}/dto/${names.kebab}.dto.ts`,
339
+ content: apply(
340
+ `import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
341
+ import { IsOptional, IsString } from 'class-validator';
342
+
343
+ export class Create{{pascal}}Dto {
344
+ @ApiProperty({ example: 'name' })
345
+ @IsString()
346
+ name!: string;
347
+ }
348
+
349
+ export class Update{{pascal}}Dto {
350
+ @ApiPropertyOptional({ example: 'name' })
351
+ @IsOptional()
352
+ @IsString()
353
+ name?: string;
354
+ }
355
+
356
+ export class Filter{{pascal}}Dto {
357
+ @ApiPropertyOptional()
358
+ @IsOptional()
359
+ page?: number;
360
+
361
+ @ApiPropertyOptional()
362
+ @IsOptional()
363
+ pageSize?: number;
364
+
365
+ @ApiPropertyOptional()
366
+ @IsOptional()
367
+ @IsString()
368
+ q?: string;
369
+ }
370
+ `,
371
+ names,
372
+ {},
373
+ ),
374
+ });
375
+ }
376
+
377
+ if (helpers) {
378
+ files.push(
379
+ {
380
+ relativePath: `${base}/helpers/${names.kebab}-validate.helper.ts`,
381
+ content: apply(
382
+ `import { Injectable } from '@nestjs/common';
383
+
384
+ import { {{pascal}}Repository } from '../repositories/{{kebab}}.repository';
385
+ import { get{{pascal}}Select } from '../types/select-{{kebab}}.type';
386
+
387
+ @Injectable()
388
+ export class {{pascal}}ValidateHelper {
389
+ constructor(private readonly {{camel}}Repository: {{pascal}}Repository) {}
390
+
391
+ async assertExists(id: string) {
392
+ return this.{{camel}}Repository.getThrowById({
393
+ id,
394
+ select: get{{pascal}}Select('minimal'),
395
+ });
396
+ }
397
+ }
398
+ `,
399
+ names,
400
+ {},
401
+ ),
402
+ },
403
+ {
404
+ relativePath: `${base}/helpers/${names.kebab}-mapper.helper.ts`,
405
+ content: apply(
406
+ `import { Injectable } from '@nestjs/common';
407
+
408
+ @Injectable()
409
+ export class {{pascal}}MapperHelper {
410
+ toResponse(entity: Record<string, unknown>) {
411
+ return entity;
412
+ }
413
+ }
414
+ `,
415
+ names,
416
+ {},
417
+ ),
418
+ },
419
+ );
420
+ }
421
+
422
+ return files;
423
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/naming.ts","../src/templates.ts","../src/commands/generate.ts","../src/commands/codegen.ts","../src/commands/validate.ts"],"sourcesContent":["export interface ModuleNames {\n kebab: string;\n camel: string;\n pascal: string;\n repoModel: string;\n route: string;\n}\n\nconst KEBAB_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\n\nexport function assertKebabName(name: string): void {\n if (!KEBAB_NAME_RE.test(name)) {\n throw new Error(\n `Invalid module name \"${name}\". Use kebab-case (e.g. product, blog-post).`,\n );\n }\n}\n\nfunction kebabToPascal(kebab: string): string {\n return kebab\n .split('-')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\nfunction kebabToCamel(kebab: string): string {\n const pascal = kebabToPascal(kebab);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\nexport function resolveNames(kebab: string, route?: string): ModuleNames {\n assertKebabName(kebab);\n const pascal = kebabToPascal(kebab);\n const camel = kebabToCamel(kebab);\n return {\n kebab,\n camel,\n pascal,\n repoModel: camel,\n route: route ?? kebab,\n };\n}\n","import type { ModuleNames } from './naming';\n\nexport type GenerateOptions = {\n names: ModuleNames;\n cacheEnabled: boolean;\n /** When false (default), only emit the repository file. */\n full?: boolean;\n /** Prisma client import path (default `@prisma/client`). */\n prismaImport?: string;\n};\n\nfunction apply(template: string, names: ModuleNames, extras: Record<string, string>): string {\n const replacements: Record<string, string> = {\n '{{pascal}}': names.pascal,\n '{{camel}}': names.camel,\n '{{kebab}}': names.kebab,\n '{{route}}': names.route,\n '{{repoModel}}': names.repoModel,\n ...extras,\n };\n let result = template;\n for (const [key, value] of Object.entries(replacements)) {\n result = result.split(key).join(value);\n }\n return result;\n}\n\nexport type GeneratedFile = {\n relativePath: string;\n content: string;\n};\n\nfunction renderRepository(\n names: ModuleNames,\n cacheEnabled: boolean,\n prismaImport: string,\n base: string,\n): GeneratedFile {\n const cacheBlock = cacheEnabled\n ? ` cache: {\n ttl: 86400,\n sensitiveFields: ['password'],\n },\n`\n : '';\n\n const content = apply(\n `import { Prisma } from '{{prismaImport}}';\nimport { createInjectableRepository } from '@prismakit/nestjs';\n\nexport const {{pascal}}Repository = createInjectableRepository({\n model: '{{repoModel}}',\n scalarFields: Prisma.{{pascal}}ScalarFieldEnum,\n{{cacheBlock}}});\n\nexport type {{pascal}}Repository = InstanceType<typeof {{pascal}}Repository>;\n`,\n names,\n {\n '{{cacheBlock}}': cacheBlock,\n '{{prismaImport}}': prismaImport,\n },\n );\n\n return {\n relativePath: `${base}/repositories/${names.kebab}.repository.ts`,\n content,\n };\n}\n\nexport function renderModuleFiles(options: GenerateOptions): GeneratedFile[] {\n const { names, cacheEnabled, full = false } = options;\n const prismaImport = options.prismaImport ?? '@prisma/client';\n const base = `src/modules/${names.kebab}`;\n\n const repository = renderRepository(names, cacheEnabled, prismaImport, base);\n\n if (!full) {\n return [repository];\n }\n\n const service = apply(\n `import { Injectable } from '@nestjs/common';\n\nimport { {{pascal}}Repository } from '../repositories/{{kebab}}.repository';\nimport { get{{pascal}}Select } from '../types/select-{{kebab}}.type';\n\n@Injectable()\nexport class {{pascal}}Service {\n constructor(private readonly {{camel}}Repository: {{pascal}}Repository) {}\n\n async handleGetById(id: string) {\n return await this.{{camel}}Repository.getThrowById({\n id,\n select: get{{pascal}}Select('general'),\n setCache: true,\n });\n }\n}\n`,\n names,\n {},\n );\n\n const controller = apply(\n `import { Controller, Get, Param } from '@nestjs/common';\n\nimport { {{pascal}}Service } from '../services/{{kebab}}.service';\n\n@Controller('{{route}}')\nexport class {{pascal}}Controller {\n constructor(private readonly {{camel}}Service: {{pascal}}Service) {}\n\n @Get(':id')\n async getById(@Param('id') id: string) {\n return this.{{camel}}Service.handleGetById(id);\n }\n}\n`,\n names,\n {},\n );\n\n const moduleFile = apply(\n `import { Module } from '@nestjs/common';\n\nimport { {{pascal}}Controller } from './controllers/{{kebab}}.controller';\nimport { {{pascal}}Service } from './services/{{kebab}}.service';\nimport { {{pascal}}Repository } from './repositories/{{kebab}}.repository';\n\n@Module({\n controllers: [{{pascal}}Controller],\n providers: [{{pascal}}Service, {{pascal}}Repository],\n exports: [{{pascal}}Service, {{pascal}}Repository],\n})\nexport class {{pascal}}Module {}\n`,\n names,\n {},\n );\n\n const select = apply(\n `import { Prisma } from '{{prismaImport}}';\n\ntype {{pascal}}SelectPresetKey = keyof typeof {{camel}}SelectPresets;\n\nexport function get{{pascal}}Select<K extends {{pascal}}SelectPresetKey>(key: K) {\n return {{camel}}SelectPresets[key];\n}\n\nexport const {{camel}}SelectPresets = {\n minimal: {\n id: true,\n } satisfies Prisma.{{pascal}}Select,\n\n general: {\n id: true,\n } satisfies Prisma.{{pascal}}Select,\n};\n`,\n names,\n { '{{prismaImport}}': prismaImport },\n );\n\n const where = apply(\n `import { Prisma } from '{{prismaImport}}';\n\nexport function where{{pascal}}GetManyPaginate(_filter: {\n q?: string;\n}): {\n where: Prisma.{{pascal}}WhereInput;\n} {\n return { where: {} };\n}\n`,\n names,\n { '{{prismaImport}}': prismaImport },\n );\n\n return [\n { relativePath: `${base}/${names.kebab}.module.ts`, content: moduleFile },\n {\n relativePath: `${base}/controllers/${names.kebab}.controller.ts`,\n content: controller,\n },\n {\n relativePath: `${base}/services/${names.kebab}.service.ts`,\n content: service,\n },\n repository,\n {\n relativePath: `${base}/types/select-${names.kebab}.type.ts`,\n content: select,\n },\n {\n relativePath: `${base}/types/where-${names.kebab}.type.ts`,\n content: where,\n },\n ];\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { resolveNames } from '../naming';\nimport { renderModuleFiles } from '../templates';\n\nexport type GenerateCommandOptions = {\n name: string;\n cache?: boolean;\n route?: string;\n cwd?: string;\n dryRun?: boolean;\n /** Emit full Nest module (controller/service/types). Default: repo-only. */\n full?: boolean;\n /** Prisma client import path (default `@prisma/client`). */\n prismaImport?: string;\n};\n\nexport function runGenerate(options: GenerateCommandOptions): void {\n const cwd = options.cwd ?? process.cwd();\n const names = resolveNames(options.name, options.route);\n const full = !!options.full;\n const files = renderModuleFiles({\n names,\n cacheEnabled: !!options.cache,\n full,\n prismaImport: options.prismaImport,\n });\n\n for (const file of files) {\n const fullPath = path.join(cwd, file.relativePath);\n if (options.dryRun) {\n console.log(`[dry-run] would write ${file.relativePath}`);\n continue;\n }\n if (fs.existsSync(fullPath)) {\n console.warn(`skip (exists): ${file.relativePath}`);\n continue;\n }\n fs.mkdirSync(path.dirname(fullPath), { recursive: true });\n const content = file.content.endsWith('\\n')\n ? file.content\n : `${file.content}\\n`;\n fs.writeFileSync(fullPath, content, 'utf-8');\n console.log(`created ${file.relativePath}`);\n }\n\n if (full) {\n console.log(\n `\\nScaffolded module \"${names.kebab}\". Register ${names.pascal}Module in app.module.ts.`,\n );\n } else {\n console.log(\n `\\nScaffolded repository \"${names.pascal}Repository\". Register it in your feature module providers.`,\n );\n }\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport {\n computeRelationAliasesFromSchema,\n getSchemaModels,\n} from '@prismakit/core';\n\nexport type CodegenCommandOptions = {\n cwd?: string;\n schemaPath?: string;\n write?: boolean;\n outFile?: string;\n};\n\n/**\n * Parse prisma/schema.prisma and print (or write) suggested relation aliases.\n */\nexport function runCodegen(options: CodegenCommandOptions = {}): void {\n const cwd = options.cwd ?? process.cwd();\n const schemaPath =\n options.schemaPath ?? path.join(cwd, 'prisma', 'schema.prisma');\n\n if (!fs.existsSync(schemaPath)) {\n throw new Error(`Prisma schema not found at ${schemaPath}`);\n }\n\n const models = getSchemaModels(schemaPath);\n const aliases = computeRelationAliasesFromSchema(models);\n\n const entries = Object.entries(aliases).sort(([a], [b]) =>\n a.localeCompare(b),\n );\n\n if (entries.length === 0) {\n console.log(\n 'No additional relation aliases suggested (suffix rules cover all).',\n );\n return;\n }\n\n const lines = [\n '// Suggested RELATION_MODEL_ALIASES entries (merge into your resolver config)',\n 'export const SUGGESTED_RELATION_MODEL_ALIASES = {',\n ...entries.map(([k, v]) => ` ${k}: '${v}',`),\n '} as const;',\n '',\n ];\n const output = lines.join('\\n');\n\n if (options.write) {\n const out =\n options.outFile ??\n path.join(\n cwd,\n 'src',\n 'infrastructure',\n 'prisma',\n 'suggested-relation-aliases.ts',\n );\n fs.mkdirSync(path.dirname(out), { recursive: true });\n fs.writeFileSync(out, output, 'utf-8');\n console.log(`wrote ${path.relative(cwd, out)}`);\n } else {\n console.log(output);\n }\n\n console.log(`\\n${entries.length} alias suggestion(s).`);\n}\n","import {\n assertSelectComposeValid,\n validateSelectCompose,\n} from '@prismakit/core';\n\nexport type ValidateCommandOptions = {\n cwd?: string;\n assert?: boolean;\n};\n\n/**\n * Run select-compose validation from @prismakit/core.\n */\nexport function runValidate(options: ValidateCommandOptions = {}): void {\n const cwd = options.cwd ?? process.cwd();\n\n if (options.assert !== false) {\n try {\n assertSelectComposeValid(cwd);\n console.log('Select compose validation passed.');\n } catch (err) {\n console.error((err as Error).message);\n process.exitCode = 1;\n }\n return;\n }\n\n const issues = validateSelectCompose(cwd);\n if (issues.length === 0) {\n console.log('Select compose validation passed.');\n return;\n }\n\n for (const issue of issues) {\n console.error(` - ${issue.file}: ${issue.message}`);\n }\n process.exitCode = 1;\n}\n"],"mappings":"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","names":["fs","path"]}