Skip to content

Instantly share code, notes, and snippets.

@mwibutsa
Created May 5, 2025 15:19
Show Gist options
  • Save mwibutsa/24173b7587825a9021233c48325f2d0c to your computer and use it in GitHub Desktop.
Save mwibutsa/24173b7587825a9021233c48325f2d0c to your computer and use it in GitHub Desktop.
Model, Repository, Service, Controller, Routers, and Docs file creation script
import sys
from pathlib import Path
import subprocess
INDEX_FILE: str = 'index.ts'
def write_file(path: Path, content: str):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
f.write(content.strip() + "\n")
def pascal_case(name: str):
return ''.join(part.capitalize() for part in name.split('_'))
def update_database_models(model_name: str):
models_file_path = Path('src/api/v1/database/models.ts')
import_line = f"export type T{model_name.capitalize()}Model = typeof prismaClient.{model_name};"
export_line = f"export const {model_name.capitalize()} = prismaClient.{model_name};"
lines = models_file_path.read_text(encoding='utf-8').splitlines()
if import_line not in lines:
lines.append(import_line)
if export_line not in lines:
lines.append(export_line)
models_file_path.write_text('\n'.join(lines) + '\n')
def update_router_index(router_dir: Path, name: str):
index_path = router_dir / INDEX_FILE
import_line = f"import {name}Router from './{name}.router';"
use_line = f"v1Router.use('/{name}s', {name}Router);"
lines = index_path.read_text(encoding="utf-8").splitlines()
if import_line not in lines:
lines.insert(0, import_line)
for i, line in enumerate(lines):
if "export default v1Router" in line:
if use_line not in lines:
lines.insert(i, use_line)
break
index_path.write_text('\n'.join(lines) + '\n')
def update_docs_index(docs_index: Path, name: str):
import_line = f"import {{ {name}Documentation }} from './{name}';"
documentation_entry = f"{name}Documentation,"
with docs_index.open("r", encoding="utf-8") as f:
lines = f.readlines()
# Add import if not present
if import_line + "\n" not in lines:
# Find the last import to insert after it
last_import_idx = 0
for i, line in enumerate(lines):
if line.strip().startswith("import "):
last_import_idx = i
lines.insert(last_import_idx + 1, import_line + "\n")
# Insert documentation entry above the closing array bracket
for i, line in enumerate(lines):
if "buildOpenApiDocument([" in line:
start_idx = i
break
else:
raise FileNotFoundError("Could not find buildOpenApiDocument([")
# Find closing ])
for j in range(start_idx, len(lines)):
if lines[j].strip() == "]);":
# Check if entry already exists
if documentation_entry not in lines[start_idx:j]:
lines.insert(j, f" {documentation_entry}\n")
break
docs_index.write_text(''.join(lines))
def main():
if len(sys.argv) < 2:
print("Usage: python new_feature.py <FeatureName>")
sys.exit(1)
raw_name = sys.argv[1]
name = raw_name.lower()
class_name = pascal_case(raw_name)
base_dir = Path("src/api/v1")
# Repositories
repo_path = base_dir / "repositories" / f"{name}.repository.ts"
repo_code = f"""
import {{T{class_name}Model,{class_name}}} from '@/api/v1/database/models'
export class {class_name}Repository {{
constructor(private readonly model: T{class_name}Model) {{}}
}}
export const {name}Repository = new {class_name}Repository({class_name});
export default {name}Repository;
"""
model_name = f"{class_name[0].lower()}{class_name[1:]}"
update_database_models(model_name)
write_file(repo_path, repo_code)
# Services
service_path = base_dir / "services" / f"{name}.service.ts"
service_code = f"""export class {class_name}Service {{
}}
export const {name}Service = new {class_name}Service();
export default {name}Service;
"""
write_file(service_path, service_code)
# Controllers
controller_path = base_dir / "controllers" / f"{name}.controller.ts"
controller_code = f"""export class {class_name}Controller {{
}}
export const {name}Controller = new {class_name}Controller();
export default {name}Controller;
"""
write_file(controller_path, controller_code)
# Router
router_path = base_dir / "router" / f"{name}.router.ts"
router_code = f"""import {{ Router }} from 'express';
const {name}Router = Router();
export default {name}Router;
"""
write_file(router_path, router_code)
update_router_index(base_dir / "router", name)
# Validations
validation_path = base_dir / "validations" / f"{name}.validations.ts"
validation_code = """import { Joi, celebrate } from 'celebrate';
"""
write_file(validation_path, validation_code)
# OpenAPI Docs
docs_module_dir = base_dir / "docs" / name
paths_code = f"""import {{ OpenAPIV3 }} from 'openapi-types';
export const {name}Paths: OpenAPIV3.PathsObject = {{
'/api/v1/{name}': {{}}
}};
"""
schemas_code = f"""import {{ OpenAPIV3 }} from 'openapi-types';
export const {name}Schemas: Record<string, OpenAPIV3.SchemaObject> = {{}};
"""
docs_index_code = f"""import {{ {name}Schemas }} from './{name}.schemas';
import {{ {name}Paths }} from './{name}.paths';
import {{ ModuleDocumentation }} from '@/config/swagger';
export const {name}Documentation: ModuleDocumentation = {{
paths: {name}Paths,
schemas: {name}Schemas,
}};
"""
write_file(docs_module_dir / f"{name}.paths.ts", paths_code)
write_file(docs_module_dir / f"{name}.schemas.ts", schemas_code)
write_file(docs_module_dir / INDEX_FILE, docs_index_code)
update_docs_index(base_dir / "docs" / INDEX_FILE, name)
# Format with yarn
subprocess.run(["yarn", "format"])
if __name__ == "__main__":
main()

Comments are disabled for this gist.