
Nestjs Drizzle Crud Generator
- 21 installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit-claude-code
This is a copy of nestjs-drizzle-crud-generator by giuseppe-trisciuoglio - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
nestjs-drizzle-crud-generator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- nestjs-drizzle-crud-generator
- AI & Agent Building
- AI-coding skill
Nestjs Drizzle Crud Generator by the numbers
- 21 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit-claude-code --skill nestjs-drizzle-crud-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 318 |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit-claude-code ↗ |
What it does
Helps with ai & agent building tasks.
Files
NestJS Drizzle CRUD Generator
Overview
Automatically generates complete CRUD modules for NestJS applications using Drizzle ORM. Creates all necessary files following the zaccheroni-monorepo patterns: feature modules, controllers, services, Zod-validated DTOs, Drizzle schemas, and Jest unit tests.
When to Use
- Creating new entity modules with full CRUD endpoints
- Building database-backed features in NestJS
- Generating type-safe DTOs with Zod validation
- Adding services with Drizzle ORM queries
- Creating unit tests with mocked database
Instructions
Step 1: Define Entity Fields
Gather entity definition:
- Entity name (e.g.,
user,product,order) - List of fields with types (see
references/field-types.mdfor supported types) - Required fields vs optional fields with defaults
Step 2: Run the Generator
python scripts/generate_crud.py --feature <name> --fields '<json-array>' --output <path>Step 3: Verify Generated Files
Check that all expected files were created:
ls -la libs/server/<feature-name>/src/lib/Expected structure:
controllers/
services/
dto/
schema/
<feature>-feature.module.tsStep 4: Run TypeScript Compilation
cd libs/server && npx tsc --noEmitStep 5: Execute Unit Tests
cd libs/server && npm test -- --testPathPattern=<feature-name>Examples
Generate a User module
python scripts/generate_crud.py \
--feature user \
--fields '[{"name": "name", "type": "string", "required": true}, {"name": "email", "type": "email", "required": true}, {"name": "password", "type": "string", "required": true}]' \
--output ./libs/serverGenerate a Product module
python scripts/generate_crud.py \
--feature product \
--fields '[{"name": "title", "type": "string", "required": true}, {"name": "price", "type": "number", "required": true}, {"name": "description", "type": "text", "required": false}, {"name": "inStock", "type": "boolean", "required": false, "default": true}]' \
--output ./libs/serverGenerated Structure
libs/server/{feature-name}/
├── src/
│ ├── index.ts
│ └── lib/
│ ├── {feature}-feature.module.ts
│ ├── controllers/
│ │ ├── index.ts
│ │ └── {feature}.controller.ts
│ ├── services/
│ │ ├── index.ts
│ │ ├── {feature}.service.ts
│ │ └── {feature}.service.spec.ts
│ ├── dto/
│ │ ├── index.ts
│ │ └── {feature}.dto.ts
│ └── schema/
│ └── {feature}.table.tsFeatures
Module
- Uses
forRootAsyncpattern for lazy configuration - Exports generated service for other modules
- Imports DatabaseModule for feature tables
Controller
- Full CRUD endpoints: POST, GET, PATCH, DELETE
- Query parameter validation for pagination
- Zod validation pipe integration
Service
- Drizzle ORM query methods
- Soft delete support (via
deletedAtcolumn) - Pagination with limit/offset
- Filtering support
- Type-safe return types
DTOs
- Zod schemas for Create and Update
- Query parameter schemas for filtering
- NestJS DTO integration
Tests
- Jest test suite
- Mocked Drizzle database
- Test cases for all CRUD operations
Manual Integration
After generation, integrate into your app module:
// app.module.ts
import { {{FeatureName}}FeatureModule } from '@your-org/server-{{feature}}';
@Module({
imports: [
{{FeatureName}}FeatureModule.forRootAsync({
useFactory: () => ({
defaultPageSize: 10,
maxPageSize: 100,
}),
}),
],
})
export class AppModule {}Dependencies
Required packages:
@nestjs/common@nestjs/coredrizzle-ormdrizzle-zodzodnestjs-zod
Best Practices
1. Verify before commit: Always run tsc --noEmit and tests before committing generated code 2. Customize services: Add business logic to generated services after validation 3. Database migrations: Create migrations separately for generated Drizzle schemas 4. Use generated types: Reference generated types in your application code 5. Review DTOs: Adjust Zod validation rules based on your API requirements
Constraints and Warnings
- Soft delete only: Delete operations use soft delete (
deletedAttimestamp). Hard deletes require manual modification - No authentication: Generated code does not include auth guards - add them based on your security requirements
- Basic CRUD only: Complex queries, transactions, or business logic must be implemented manually
- JSON escaping: Use single quotes around the JSON array when passing fields on command line
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ZodCustomPipe } from '@your-org/server-utils';
import { {{FeatureName}}Service } from './{{featureName}}.service';
import {
Create{{FeatureName}}Dto,
Create{{FeatureName}}Schema,
Update{{FeatureName}}Dto,
Update{{FeatureName}}Schema,
FindAll{{FeatureName}}QueryDto,
FindAll{{FeatureName}}QuerySchema,
} from './dto/{{featureName}}.dto';
@ApiTags('{{featureName}}')
@Controller('{{featureName}}')
export class {{FeatureName}}Controller {
constructor(private readonly service: {{FeatureName}}Service) {}
@Post()
async create(
@Body(new ZodCustomPipe(Create{{FeatureName}}Schema))
dto: Create{{FeatureName}}Dto,
) {
return this.service.create(dto);
}
@Get()
async findAll(
@Query(new ZodCustomPipe(FindAll{{FeatureName}}QuerySchema))
query: FindAll{{FeatureName}}QueryDto,
) {
return this.service.findAll(query);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.service.findOne(id);
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body(new ZodCustomPipe(Update{{FeatureName}}Schema))
dto: Update{{FeatureName}}Dto,
) {
return this.service.update(id, dto);
}
@Delete(':id')
async remove(@Param('id') id: string) {
return this.service.remove(id);
}
}
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod/dto';
export const Create{{FeatureName}}Schema = z.object({
{{CreateFields}}
}).strict();
export const Update{{FeatureName}}Schema = Create{{FeatureName}}Schema.partial();
export const FindAll{{FeatureName}}QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(10),
{{FilterFields}}
}).strict();
export type Create{{FeatureName}}Dto = z.infer<typeof Create{{FeatureName}}Schema>;
export type Update{{FeatureName}}Dto = z.infer<typeof Update{{FeatureName}}Schema>;
export type FindAll{{FeatureName}}QueryDto = z.infer<typeof FindAll{{FeatureName}}QuerySchema>;
export class Create{{FeatureName}}Dto extends createZodDto(Create{{FeatureName}}Schema) {}
export class Update{{FeatureName}}Dto extends createZodDto(Update{{FeatureName}}Schema) {}
export class FindAll{{FeatureName}}QueryDto extends createZodDto(FindAll{{FeatureName}}QuerySchema) {}
import { DynamicModule, Module } from '@nestjs/common';
import { AsyncFeatureModuleOptions } from '@nestjs/common/interfaces/features/async-options.interface';
import { DatabaseModule } from '@your-org/server-database';
import { {{FeatureName}}Controller } from './controllers';
import { {{FeatureName}}Service } from './services';
import { {{FeatureName}}Table } from './schema';
export const FEATURE_OPTIONS = '{{featureName}}.feature-options';
export interface {{FeatureName}}FeatureOptions {
defaultPageSize?: number;
maxPageSize?: number;
includeDeleted?: boolean;
}
export type Async{{FeatureName}}ModuleOptions = AsyncFeatureModuleOptions<{{FeatureName}}FeatureOptions>;
@Module({})
export class {{FeatureName}}FeatureModule {
static forRootAsync(options: Async{{FeatureName}}ModuleOptions): DynamicModule {
return {
global: true,
module: {{FeatureName}}FeatureModule,
imports: [DatabaseModule.forFeature([{{FeatureName}}Table])],
controllers: [{{FeatureName}}Controller],
providers: [
{
provide: FEATURE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject,
},
{{FeatureName}}Service,
],
exports: [{{FeatureName}}Service],
};
}
}
import { Injectable, NotFoundException } from '@nestjs/common';
import { Inject } from '@nestjs/common/decorators';
import { eq, isNull, and } from 'drizzle-orm';
import { DrizzleD1Database, DrizzlePgDatabase } from 'drizzle-orm/cloud';
import { {{FeatureName}}Table } from '../schema/{{featureName}}.table';
import {
Create{{FeatureName}}Dto,
Update{{FeatureName}}Dto,
FindAll{{FeatureName}}QueryDto,
} from './dto/{{featureName}}.dto';
export const DrizzleProvider = Symbol('DrizzleProvider');
@Injectable()
export class {{FeatureName}}Service {
private readonly defaultPageSize: number;
private readonly maxPageSize: number;
private readonly includeDeleted: boolean;
constructor(
@Inject(DrizzleProvider)
private readonly db: DrizzleD1Database | DrizzlePgDatabase,
) {
this.defaultPageSize = 10;
this.maxPageSize = 100;
this.includeDeleted = false;
}
async create(dto: Create{{FeatureName}}Dto): Promise<{{FeatureName}}> {
const [created] = await this.db
.insert({{FeatureName}}Table)
.values(dto as any)
.returning();
return created as {{FeatureName}};
}
async findAll(
query: FindAll{{FeatureName}}QueryDto,
): Promise<{ rows: {{FeatureName}}[]; total: number }> {
const { page = 1, limit = this.defaultPageSize, ...filters } = query;
const offset = (page - 1) * Math.min(limit, this.maxPageSize);
const whereConditions = [];
if (!this.includeDeleted) {
whereConditions.push(isNull({{FeatureName}}Table.deletedAt));
}
// Apply filters
for (const [key, value] of Object.entries(filters)) {
if (value !== undefined && value !== null) {
whereConditions.push(
// @ts-ignore - dynamic field access
eq({{FeatureName}}Table[key], value),
);
}
}
const whereClause = and(...whereConditions);
const [rows, countResult] = await Promise.all([
this.db
.select()
.from({{FeatureName}}Table)
.where(whereClause)
.limit(Math.min(limit, this.maxPageSize))
.offset(offset),
this.db
.select({ count: {{FeatureName}}Table.id })
.from({{FeatureName}}Table)
.where(whereClause),
]);
const total = countResult.length;
return {
rows: rows as {{FeatureName}}[],
total,
};
}
async findOne(id: string): Promise<{{FeatureName}} | null> {
const conditions = [eq({{FeatureName}}Table.id, id)];
if (!this.includeDeleted) {
conditions.push(isNull({{FeatureName}}Table.deletedAt));
}
const [result] = await this.db
.select()
.from({{FeatureName}}Table)
.where(and(...conditions));
return (result as {{FeatureName}}) || null;
}
async update(
id: string,
dto: Update{{FeatureName}}Dto,
): Promise<{{FeatureName}}> {
const existing = await this.findOne(id);
if (!existing) {
throw new NotFoundException(`{{FeatureName}} with id ${id} not found`);
}
const [updated] = await this.db
.update({{FeatureName}}Table)
.set({ ...dto, updatedAt: new Date() } as any)
.where(eq({{FeatureName}}Table.id, id))
.returning();
return updated as {{FeatureName}};
}
async remove(id: string): Promise<void> {
const existing = await this.findOne(id);
if (!existing) {
throw new NotFoundException(`{{FeatureName}} with id ${id} not found`);
}
// Soft delete
await this.db
.update({{FeatureName}}Table)
.set({ deletedAt: new Date() } as any)
.where(eq({{FeatureName}}Table.id, id));
}
}
import { pgTable, text, timestamp, uuid, boolean, integer, real } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const {{FeatureName}}Table = pgTable('{{tableName}}', {
id: uuid('id').primaryKey().defaultRandom(),
{{TableFields}}{{TableFieldsSuffix}}
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
deletedAt: timestamp('deleted_at'),
});
export const {{FeatureName}}Relations = relations({{FeatureName}}Table, ({ many }) => ({
{{RelationFields}}
}));
export type {{FeatureName}} = typeof {{FeatureName}}Table.$inferSelect;
export type New{{FeatureName}} = typeof {{FeatureName}}Table.$inferInsert;
import { Test, TestingModule } from '@nestjs/testing';
import { {{FeatureName}}Service } from './{{featureName}}.service';
import { DrizzleProvider } from './{{featureName}}.service';
import { {{FeatureName}}Table } from '../schema/{{featureName}}.table';
describe('{{FeatureName}}Service', () => {
let service: {{FeatureName}}Service;
let mockDb: {
select: jest.Mock;
insert: jest.Mock;
update: jest.Mock;
delete: jest.Mock;
};
const mock{{FeatureName}} = {
id: '123e4567-e89b-12d3-a456-426614174000',
{{MockFields}}
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
};
beforeEach(async () => {
mockDb = {
select: jest.fn().mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
limit: jest.fn().mockReturnValue({
offset: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
}),
}),
}),
}),
}),
insert: jest.fn().mockReturnValue({
values: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([mock{{FeatureName}}]),
}),
}),
update: jest.fn().mockReturnValue({
set: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([mock{{FeatureName}}]),
}),
}),
}),
delete: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
}),
}),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
{{FeatureName}}Service,
{
provide: DrizzleProvider,
useValue: mockDb,
},
],
}).compile();
service = module.get<{{FeatureName}}Service>({{FeatureName}}Service);
});
describe('create', () => {
it('should create a new {{featureName}}', async () => {
const dto = {{CreateDtoMock}};
const result = await service.create(dto as any);
expect(mockDb.insert).toHaveBeenCalled();
expect(result).toEqual(mock{{FeatureName}});
});
});
describe('findAll', () => {
it('should return paginated results', async () => {
const query = { page: 1, limit: 10 };
const result = await service.findAll(query as any);
expect(result.rows).toEqual([mock{{FeatureName}}]);
expect(result.total).toBeGreaterThanOrEqual(0);
});
it('should return empty array when no results', async () => {
mockDb.select.mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
limit: jest.fn().mockReturnValue({
offset: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
}),
}),
}),
}),
});
const query = { page: 1, limit: 10 };
const result = await service.findAll(query as any);
expect(result.rows).toEqual([]);
expect(result.total).toBe(0);
});
});
describe('findOne', () => {
it('should return a {{featureName}} by id', async () => {
const result = await service.findOne(mock{{FeatureName}}.id);
expect(result).toEqual(mock{{FeatureName}});
});
it('should return null for non-existent id', async () => {
mockDb.select.mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
}),
}),
});
const result = await service.findOne('non-existent-id');
expect(result).toBeNull();
});
});
describe('update', () => {
it('should update a {{featureName}}', async () => {
const dto = {{UpdateDtoMock}};
const result = await service.update(mock{{FeatureName}}.id, dto as any);
expect(mockDb.update).toHaveBeenCalled();
expect(result).toEqual(mock{{FeatureName}});
});
it('should throw NotFoundException for non-existent id', async () => {
mockDb.select.mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
}),
}),
});
await expect(
service.update('non-existent-id', {} as any),
).rejects.toThrow('not found');
});
});
describe('remove', () => {
it('should soft delete a {{featureName}}', async () => {
await service.remove(mock{{FeatureName}}.id);
expect(mockDb.update).toHaveBeenCalled();
});
it('should throw NotFoundException for non-existent id', async () => {
mockDb.select.mockReturnValue({
from: jest.fn().mockReturnValue({
where: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([]),
}),
}),
});
await expect(service.remove('non-existent-id')).rejects.toThrow(
'not found',
);
});
});
});
Supported Field Types
Field Type Mapping
| Type | Drizzle Column | Zod Schema |
|---|---|---|
| string | text | z.string() |
| text | text | z.string() |
| number | real | z.number() |
| integer | integer | z.number().int() |
| boolean | boolean | z.boolean() |
| date | timestamp | z.date() |
| uuid | uuid | z.string().uuid() |
| text | z.string().email() |
Field Options
Each field definition supports these properties:
| Property | Type | Description |
|---|---|---|
| name | string | Field name (camelCase recommended) |
| type | string | Data type from the table above |
| required | boolean | Whether the field is mandatory |
| default | any | Default value for optional fields |
| maxLength | number | Maximum length for string/text fields |
| minLength | number | Minimum length for string/text fields |
Example Field Definitions
[
{"name": "name", "type": "string", "required": true},
{"name": "email", "type": "email", "required": true},
{"name": "age", "type": "integer", "required": false},
{"name": "isActive", "type": "boolean", "required": false, "default": true},
{"name": "price", "type": "number", "required": true},
{"name": "bio", "type": "text", "required": false},
{"name": "userId", "type": "uuid", "required": false}
]#!/usr/bin/env python3
"""
NestJS Drizzle CRUD Generator
Usage:
generate_crud.py --feature <feature-name> --fields <fields-json> [--output <output-dir>]
Example:
generate_crud.py --feature user --fields '[{"name": "name", "type": "string", "required": true}, {"name": "email", "type": "string", "required": true}]' --output ./libs/server
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Any
def to_camel_case(text: str) -> str:
"""Convert to camelCase"""
components = text.replace('-', '_').split('_')
return components[0] + ''.join(x.title() for x in components[1:])
def to_pascal_case(text: str) -> str:
"""Convert to PascalCase"""
return ''.join(x.title() for x in text.replace('-', '_').split('_'))
def to_snake_case(text: str) -> str:
"""Convert to snake_case"""
return re.sub(r'(?<!^)(?=[A-Z])', '_', text).lower()
def read_template(template_name: str) -> str:
"""Read template file from assets/templates directory"""
script_dir = Path(__file__).parent.parent
template_path = script_dir / 'assets' / 'templates' / template_name
if not template_path.exists():
raise FileNotFoundError(f"Template not found: {template_path}")
return template_path.read_text()
def map_field_type_to_drizzle(field_type: str) -> str:
"""Map TypeScript types to Drizzle column types"""
type_mapping = {
'string': 'text',
'text': 'text',
'number': 'real',
'integer': 'integer',
'boolean': 'boolean',
'date': 'timestamp',
'uuid': 'uuid',
'email': 'text',
}
return type_mapping.get(field_type, 'text')
def map_field_type_to_zod(field_type: str) -> str:
"""Map field types to Zod schema types"""
type_mapping = {
'string': 'z.string()',
'text': 'z.string()',
'number': 'z.number()',
'integer': 'z.number().int()',
'boolean': 'z.boolean()',
'date': 'z.date()',
'uuid': 'z.string().uuid()',
'email': 'z.string().email()',
}
return type_mapping.get(field_type, 'z.string()')
def generate_table_fields(fields: list[dict]) -> str:
"""Generate table field definitions"""
lines = []
for field in fields:
name = field['name']
field_type = field.get('type', 'string')
required = field.get('required', False)
default = field.get('default')
drizzle_type = map_field_type_to_drizzle(field_type)
if field_type == 'uuid':
line = f" {name}: {drizzle_type}('{name}').primaryKey().defaultRandom()"
elif field_type == 'boolean':
line = f" {name}: {drizzle_type}('{name}')"
if default is not None:
line += f".default({default})"
elif required:
line += ".notNull()"
elif field_type == 'number' or field_type == 'integer':
line = f" {name}: {drizzle_type}('{name}')"
if default is not None:
line += f".default({default})"
elif required:
line += ".notNull()"
else:
line = f" {name}: {drizzle_type}('{name}')"
if default:
line += f".default('{default}')"
elif required:
line += ".notNull()"
lines.append(line)
if not lines:
return ''
# Add comma to all lines except the last one
lines_with_commas = [line + ',' for line in lines[:-1]]
if lines:
lines_with_commas.append(lines[-1])
return '\n'.join(lines_with_commas)
def generate_create_fields(fields: list[dict]) -> str:
"""Generate create schema fields"""
lines = []
for field in fields:
name = field['name']
field_type = field.get('type', 'string')
required = field.get('required', False)
max_length = field.get('maxLength')
min_length = field.get('minLength')
zod_type = map_field_type_to_zod(field_type)
# Build chain
if not required:
zod_type = zod_type.replace('z.', 'z.')
chain = zod_type
if min_length:
chain += f".min({min_length})"
if max_length:
chain += f".max({max_length})"
if not required:
chain += ".optional()"
lines.append(f" {name}: {chain}")
return ',\n'.join(lines)
def generate_filter_fields(fields: list[dict]) -> str:
"""Generate filter query fields"""
lines = []
for field in fields:
name = field['name']
lines.append(f" {name}: z.{map_field_type_to_zod(field.get('type', 'string')).replace('z.', '')}")
return ',\n'.join(lines)
def generate_mock_fields(fields: list[dict]) -> str:
"""Generate mock fields for tests"""
lines = []
for field in fields:
name = field['name']
field_type = field.get('type', 'string')
if field_type == 'boolean':
lines.append(f" {name}: true,")
elif field_type == 'number' or field_type == 'integer':
lines.append(f" {name}: 1,")
elif field_type == 'date':
lines.append(f" {name}: new Date(),")
else:
lines.append(f" {name}: 'test_{name}',")
return '\n'.join(lines)
def generate_create_dto(fields: list[dict]) -> str:
"""Generate create DTO mock for tests"""
lines = []
for field in fields:
name = field['name']
field_type = field.get('type', 'string')
if field_type == 'boolean':
lines.append(f" {name}: true,")
elif field_type == 'number' or field_type == 'integer':
lines.append(f" {name}: 1,")
elif field_type == 'date':
lines.append(f" {name}: new Date(),")
else:
lines.append(f" {name}: 'test_{name}',")
return '\n'.join(lines)
def generate_update_dto(fields: list[dict]) -> str:
"""Generate update DTO mock for tests (all optional)"""
lines = []
for field in fields:
name = field['name']
field_type = field.get('type', 'string')
if field_type == 'boolean':
lines.append(f" {name}: true,")
elif field_type == 'number' or field_type == 'integer':
lines.append(f" {name}: 1,")
else:
lines.append(f" {name}: 'updated_{name}',")
return '\n'.join(lines)
def generate_relation_fields(fields: list[dict]) -> str:
"""Generate relation fields for Drizzle"""
lines = []
for field in fields:
if field.get('relation') == 'hasMany':
related = to_pascal_case(field.get('related', field['name'] + 's'))
lines.append(f" {to_camel_case(field['name'])}: many({related}Table),")
return '\n'.join(lines) if lines else ' // No relations defined'
def replace_placeholders(template: str, feature_name: str, fields: list[dict]) -> str:
"""Replace all placeholders in template"""
pascal_name = to_pascal_case(feature_name)
camel_name = to_camel_case(feature_name)
snake_name = to_snake_case(feature_name)
table_name = snake_name + 's'
result = template
# Replace FeatureName -> PascalCase
result = result.replace('{{FeatureName}}', pascal_name)
# Replace featureName -> camelCase
result = result.replace('{{featureName}}', camel_name)
# Replace tableName
result = result.replace('{{tableName}}', table_name)
# Generate field-specific content
result = result.replace('{{TableFields}}', generate_table_fields(fields))
result = result.replace('{{TableFieldsSuffix}}', ',\n' if fields else '')
result = result.replace('{{CreateFields}}', generate_create_fields(fields))
result = result.replace('{{FilterFields}}', generate_filter_fields(fields))
result = result.replace('{{MockFields}}', generate_mock_fields(fields))
result = result.replace('{{CreateDtoMock}}', '{' + generate_create_dto(fields) + '}')
result = result.replace('{{UpdateDtoMock}}', '{' + generate_update_dto(fields) + '}')
result = result.replace('{{RelationFields}}', generate_relation_fields(fields))
return result
def create_directory_structure(base_path: str, feature_name: str) -> Path:
"""Create the NestJS module directory structure"""
camel_name = to_camel_case(feature_name)
base = Path(base_path)
# Create directory structure
src_dir = base / camel_name / 'src'
lib_dir = src_dir / 'lib'
controllers_dir = lib_dir / 'controllers'
services_dir = lib_dir / 'services'
dto_dir = lib_dir / 'dto'
schema_dir = lib_dir / 'schema'
for dir_path in [src_dir, lib_dir, controllers_dir, services_dir, dto_dir, schema_dir]:
dir_path.mkdir(parents=True, exist_ok=True)
return lib_dir
def generate_files(feature_name: str, fields: list[dict], output_dir: str):
"""Generate all CRUD files"""
feature_dir = create_directory_structure(output_dir, feature_name)
camel_name = to_camel_case(feature_name)
pascal_name = to_pascal_case(feature_name)
# Generate module
module_template = read_template('module-template.ts')
module_content = replace_placeholders(module_template, feature_name, fields)
(feature_dir / f'{camel_name}-feature.module.ts').write_text(module_content)
# Generate controller
controller_template = read_template('controller-template.ts')
controller_content = replace_placeholders(controller_template, feature_name, fields)
(feature_dir / 'controllers' / f'{camel_name}.controller.ts').write_text(controller_content)
# Generate controller index
(feature_dir / 'controllers' / 'index.ts').write_text(
f"export * from './{camel_name}.controller';\n"
)
# Generate service
service_template = read_template('service-template.ts')
service_content = replace_placeholders(service_template, feature_name, fields)
(feature_dir / 'services' / f'{camel_name}.service.ts').write_text(service_content)
# Generate service index
(feature_dir / 'services' / 'index.ts').write_text(
f"export * from './{camel_name}.service';\n"
)
# Generate DTO
dto_template = read_template('dto-template.ts')
dto_content = replace_placeholders(dto_template, feature_name, fields)
(feature_dir / 'dto' / f'{camel_name}.dto.ts').write_text(dto_content)
# Generate DTO index
(feature_dir / 'dto' / 'index.ts').write_text(
f"export * from './{camel_name}.dto';\n"
)
# Generate table/schema
table_template = read_template('table-template.ts')
table_content = replace_placeholders(table_template, feature_name, fields)
(feature_dir / 'schema' / f'{camel_name}.table.ts').write_text(table_content)
# Generate index
(feature_dir / 'index.ts').write_text(
f"""export * from './{camel_name}-feature.module';
export * from './controllers';
export * from './services';
export * from './dto';
export * from './schema/{camel_name}.table';
"""
)
# Generate test
test_template = read_template('test-template.ts')
test_content = replace_placeholders(test_template, feature_name, fields)
(feature_dir / 'services' / f'{camel_name}.service.spec.ts').write_text(test_content)
# Generate src index
src_dir = feature_dir.parent.parent
(src_dir / 'index.ts').write_text(
f"export * from './lib';\n"
)
print(f"✅ Generated CRUD module for '{pascal_name}' at {feature_dir}")
def main():
parser = argparse.ArgumentParser(description='Generate NestJS Drizzle CRUD module')
parser.add_argument('--feature', required=True, help='Feature name (e.g., user, product)')
parser.add_argument('--fields', required=True, help='JSON array of field definitions')
parser.add_argument('--output', default='./libs/server', help='Output directory')
args = parser.parse_args()
try:
fields = json.loads(args.fields)
except json.JSONDecodeError as e:
print(f"❌ Error parsing fields JSON: {e}")
sys.exit(1)
if not isinstance(fields, list):
print("❌ Error: fields must be a JSON array")
sys.exit(1)
generate_files(args.feature, fields, args.output)
if __name__ == '__main__':
main()