Plantilla para módulos

This commit is contained in:
David Arranz 2025-09-27 21:28:50 +02:00
parent d837e4d362
commit c5ed10b8fd
41 changed files with 1079 additions and 139 deletions

43
docs/TEMPLATE.md Normal file
View File

@ -0,0 +1,43 @@
### Prompt para tu README / CLI
**Generar un nuevo módulo ERP desde la plantilla**
Nuestro ERP permite crear módulos de forma **estandarizada** usando una **plantilla base** (Hexagonal/DDD, TS, Express, Sequelize) y un **generador Plop**.
**Opción A — con Plop (recomendada)**
1. Instala dependencias de desarrollo si no las tienes:
* `pnpm add -D plop`
2. Asegúrate de tener en el repo:
* `plopfile.js` con el generador `module`
* Carpeta `templates/new-module/` con la plantilla (placeholders Handlebars: `{{kebabCase name}}`, `{{pascalCase name}}`, etc.)
3. Ejecuta el generador:
* `pnpm plop module`
4. Introduce el nombre del módulo en **kebab-case** (p. ej., `suppliers`).
5. Se creará `packages/suppliers/` con:
* `src/api/{application,domain,infrastructure,helpers}` + `common`
* Casos de uso CRUD, presenters (FULL/LIST), specs comunes y contratos de repositorio.
6. Integra en el monorepo:
* Añade targets en `turbo.json` si aplica.
* Ejecuta `pnpm -w build` para validar compilación.
**Opción B — manual (desde ZIP de plantilla)**
1. Descomprime `new-module-template.zip`.
2. Copia `new-module/` a `packages/<mi-modulo>/`.
3. Renombra los archivos con `entity`/`{{name}}` por el nombre real o usa búsqueda global.
4. Ajusta `package.json` (`name`, `scripts`), y registra el módulo en `turbo.json`.
5. `pnpm -w install && pnpm -w build`.
**Convenciones clave**
* Nombres de archivos/directorios en **kebab-case**; código en **Inglés**; comentarios **TSDoc en castellano**.
* Mantén SOLID, SoC, alta cohesión/bajo acoplamiento, y hexagonal (puertos/adaptadores).
* Observabilidad (logs estructurados), idempotencia en endpoints, y principio de menor privilegio.

View File

@ -1,7 +1,11 @@
{
"name": "uecko-erp-2025",
"private": true,
"workspaces": ["apps/*", "modules/*", "packages/*"],
"workspaces": [
"apps/*",
"modules/*",
"packages/*"
],
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
@ -18,6 +22,7 @@
"@biomejs/biome": "1.9.4",
"@repo/typescript-config": "workspace:*",
"inquirer": "^12.5.2",
"plop": "^4.0.4",
"ts-node": "^10.9.2",
"turbo": "^2.5.1",
"typescript": "5.8.3"

26
plopfile.js Normal file
View File

@ -0,0 +1,26 @@
export default function (plop) {
plop.setGenerator("module", {
description: "Crea un nuevo módulo ERP con plantilla base",
prompts: [
{
type: "input",
name: "name",
message: "Nombre del módulo (kebab-case):",
},
{
type: "input",
name: "plural",
message: "Nombre plural para las rutas (kebab-case):",
default: (answers) => `${plop.getHelper("kebabCase")(answers.name)}s`,
},
],
actions: [
{
type: "addMany",
destination: "modules/{{kebabCase name}}",
base: "templates/new-module",
templateFiles: "templates/new-module/**",
},
],
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,65 +0,0 @@
import fs from "fs";
import inquirer from "inquirer";
import path from "path";
function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
async function main() {
let rawName = process.argv[2];
if (!rawName) {
const answers = await inquirer.prompt([
{
type: "input",
name: "packageName",
message: "Nombre del nuevo package:",
validate: (input) => (input ? true : "El nombre no puede estar vacío"),
},
]);
rawName = answers.packageName;
}
const name = rawName.toLowerCase();
const capitalized = capitalize(name);
const packagePath = path.resolve(__dirname, "../packages", name);
const clientPath = path.join(packagePath, "client");
const serverPath = path.join(packagePath, "server");
const templatePath = path.resolve(__dirname, "./templates");
// Crear carpetas
fs.mkdirSync(clientPath, { recursive: true });
fs.mkdirSync(serverPath, { recursive: true });
// Función de reemplazo de contenido
const renderTemplate = (content: string) =>
content
.replace(/__PACKAGE_NAME__/g, name)
.replace(/__PACKAGE_NAME_CAPITALIZED__/g, capitalized);
// Copiar plantillas desde carpeta 'templates/client'
const copyFromTemplate = (srcDir: string, destDir: string) => {
const files = fs.readdirSync(srcDir);
for (const file of files) {
const filePath = path.join(srcDir, file);
const content = fs.readFileSync(filePath, "utf-8");
const outputName = file.replace(/__PACKAGE_NAME__/g, capitalized);
const outputPath = path.join(destDir, outputName);
fs.writeFileSync(outputPath, renderTemplate(content));
}
};
copyFromTemplate(path.join(templatePath, "client"), clientPath);
copyFromTemplate(path.join(templatePath, "server"), serverPath);
// package.json
const pkgJsonTemplate = fs.readFileSync(path.join(templatePath, "package.json"), "utf-8");
fs.writeFileSync(path.join(packagePath, "package.json"), renderTemplate(pkgJsonTemplate));
console.log(`✅ Package '${name}' creado correctamente en packages/${name}`);
}
main();

View File

@ -1,3 +0,0 @@
export default function __PACKAGE_NAME_CAPITALIZED__Page() {
return <div>__PACKAGE_NAME_CAPITALIZED__ Package Page</div>;
}

View File

@ -1,12 +0,0 @@
import { IPackageClient } from "@packages/package";
import __PACKAGE_NAME_CAPITALIZED__Page from "./__PACKAGE_NAME__Page";
export const __PACKAGE_NAME_CAPITALIZED__Package: IPackageClient = {
metadata: {
name: "__PACKAGE_NAME__",
route: "/__PACKAGE_NAME__",
version: "1.0.0",
description: "__PACKAGE_NAME_CAPITALIZED__ package",
},
component: __PACKAGE_NAME_CAPITALIZED__Page,
};

View File

@ -1,4 +0,0 @@
{
"extends": "@repo/typescript-config/react",
"include": ["."]
}

View File

@ -1,23 +0,0 @@
{
"name": "@packages/__PACKAGE_NAME__",
"version": "1.0.0",
"private": true,
"main": "server/index.ts",
"scripts": {
"build": "tsc -b",
"dev": "turbo run dev --filter=@packages/__PACKAGE_NAME__"
},
"dependencies": {},
"devDependencies": {},
"peerDependencies": {
"@types/express": "*",
"@types/node": "*",
"@types/react": "*",
"express": "*",
"react": "*",
"react-router-dom": "*",
"sequelize": "*",
"typescript": "*",
"zod": "*"
}
}

View File

@ -1,3 +0,0 @@
export function __PACKAGE_NAME__Controller(req, res) {
res.send("__PACKAGE_NAME_CAPITALIZED__ package response");
}

View File

@ -1,13 +0,0 @@
import { IPackageServer } from "@packages/package";
import { __PACKAGE_NAME__Controller } from "./controller";
export const __PACKAGE_NAME_CAPITALIZED__Package: IPackageServer = {
metadata: {
name: "__PACKAGE_NAME__",
version: "1.0.0",
dependencies: [],
},
init(app) {
app.get("/__PACKAGE_NAME__", __PACKAGE_NAME__Controller);
},
};

View File

@ -1,10 +0,0 @@
{
"extends": "@repo/typescript-config/node",
"include": ["."],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}

View File

@ -0,0 +1,9 @@
{
"name": "@erp/{{kebabCase name}}",
"version": "0.1.0",
"main": "src/index.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts"
}
}

View File

@ -0,0 +1,12 @@
import { Presenter } from "@erp/core/api";
import { {{pascalCase name}} } from "../../../domain";
export class {{pascalCase name}}FullPresenter extends Presenter<{{pascalCase name}}, any> {
toOutput(entity: {{pascalCase name}}): any {
return {
id: entity.id.toPrimitive(),
name: entity.name,
metadata: { entity: "{{kebabCase name}}" }
};
}
}

View File

@ -0,0 +1,22 @@
import { Presenter } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import { Collection } from "@repo/rdx-utils";
import { {{pascalCase name}} } from "../../../domain";
export class List{{pascalCase name}}sPresenter extends Presenter {
private _mapEntity(entity: {{pascalCase name}}) {
return { id: entity.id.toPrimitive(), name: entity.name };
}
toOutput(params: { entities: Collection<{{pascalCase name}}>; criteria: Criteria }): any {
const { entities, criteria } = params;
return {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(entities.total() / criteria.pageSize),
total_items: entities.total(),
items: entities.map((e) => this._mapEntity(e)),
metadata: { entity: "{{kebabCase name}}s", criteria: criteria.toJSON() }
};
}
}

View File

@ -0,0 +1,17 @@
import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd";
import { I{{pascalCase name}}Repository } from "../../domain/repositories";
export class {{pascalCase name}}NotExistsSpecification extends CompositeSpecification<UniqueID> {
constructor(
private readonly repo: I{{pascalCase name}}Repository,
private readonly transaction?: any
) {
super();
}
async isSatisfiedBy(entityId: UniqueID): Promise<boolean> {
const existsOrError = await this.repo.existsById(entityId, this.transaction);
if (existsOrError.isFailure) throw existsOrError.error;
return existsOrError.data === false;
}
}

View File

@ -0,0 +1,18 @@
import { CompositeSpecification } from "@repo/rdx-ddd";
import { I{{pascalCase name}}Repository } from "../../domain/repositories";
export class {{pascalCase name}}UniqueNameSpecification extends CompositeSpecification<string> {
constructor(
private readonly repo: I{{pascalCase name}}Repository,
private readonly transaction?: any
) {
super();
}
async isSatisfiedBy(name: string): Promise<boolean> {
const criteria = { filters: { name } };
const resultOrError = await this.repo.findByCriteria(criteria as any, this.transaction);
if (resultOrError.isFailure) throw resultOrError.error;
return resultOrError.data.total() === 0;
}
}

View File

@ -0,0 +1,28 @@
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import { I{{pascalCase name}}Repository } from "../../domain/repositories";
import { {{pascalCase name}} } from "../../domain/aggregates";
import { {{pascalCase name}}FullPresenter } from "../presenters";
type Create{{pascalCase name}}Input = { name: string };
export class Create{{pascalCase name}}UseCase {
constructor(
private readonly repo: I{{pascalCase name}}Repository,
private readonly txManager: ITransactionManager,
private readonly presenters: IPresenterRegistry
) {}
public execute(params: Create{{pascalCase name}}Input) {
return this.txManager.complete(async (tx) => {
const entityOrError = {{pascalCase name}}.create({ name: params.name });
if (entityOrError.isFailure) return Result.fail(entityOrError.error);
const savedOrError = await this.repo.save(entityOrError.data, tx);
if (savedOrError.isFailure) return Result.fail(savedOrError.error);
const presenter = this.presenters.getPresenter({ resource: "{{kebabCase name}}", projection: "FULL" }) as {{pascalCase name}}FullPresenter;
return Result.ok(presenter.toOutput(savedOrError.data));
});
}
}

View File

@ -0,0 +1,17 @@
import { AggregateRoot, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
export interface I{{pascalCase name}}Props {
name: string;
}
export class {{pascalCase name}} extends AggregateRoot<I{{pascalCase name}}Props> {
static create(props: I{{pascalCase name}}Props, id?: UniqueID): Result<{{pascalCase name}}, Error> {
const entity = new {{pascalCase name}}(props, id);
return Result.ok(entity);
}
get name(): string {
return this.props.name;
}
}

View File

@ -0,0 +1,12 @@
import { UniqueID } from "@repo/rdx-ddd";
import { Result, Collection } from "@repo/rdx-utils";
import { Criteria } from "@repo/rdx-criteria/server";
import { {{pascalCase name}} } from "../aggregates";
export interface I{{pascalCase name}}Repository {
save(entity: {{pascalCase name}}, transaction: any): Promise<Result<{{pascalCase name}}, Error>>;
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
getById(id: UniqueID, transaction?: any): Promise<Result<{{pascalCase name}}, Error>>;
findByCriteria(criteria: Criteria, transaction?: any): Promise<Result<Collection<{{pascalCase name}}>, Error>>;
deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>>;
}

View File

@ -0,0 +1,32 @@
import { ExpressController, authGuard, tenantGuard, forbidQueryFieldGuard } from "@erp/core/api";
import { Create{{pascalCase name}}UseCase } from "../../../application";
/**
* Controlador de creación
* @remarks
* - Aplica guards de autenticación, tenant y prohíbe `companyId` en query.
*/
export class Create{{pascalCase name}}Controller extends ExpressController {
public constructor(private readonly useCase: Create{{pascalCase name}}UseCase) {
super();
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) return this.forbiddenError("Tenant ID not found");
// TODO: descomentar cuando exista DTO
// const validation = validateRequest(Create{{pascalCase name}}Dto, this.req.body);
// if (validation.isErr()) return this.badRequestError(validation.error.message);
// const dto = validation.value;
const dto = this.req.body;
const result = await this.useCase.execute({ companyId, ...dto });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,25 @@
import { ExpressController, authGuard, tenantGuard, forbidQueryFieldGuard } from "@erp/core/api";
import { Delete{{pascalCase name}}UseCase } from "../../../application";
/**
* Controlador de borrado lógico/físico según políticas
*/
export class Delete{{pascalCase name}}Controller extends ExpressController {
public constructor(private readonly useCase: Delete{{pascalCase name}}UseCase) {
super();
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) return this.forbiddenError("Tenant ID not found");
const { {{snakeCase name}}_id } = this.req.params as { {{snakeCase name}}_id: string };
const result = await this.useCase.execute({ {{snakeCase name}}_id, companyId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,25 @@
import { ExpressController, authGuard, tenantGuard, forbidQueryFieldGuard } from "@erp/core/api";
import { Get{{pascalCase name}}UseCase } from "../../../application";
/**
* Controlador de lectura por id en el contexto del tenant
*/
export class Get{{pascalCase name}}Controller extends ExpressController {
public constructor(private readonly useCase: Get{{pascalCase name}}UseCase) {
super();
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) return this.forbiddenError("Tenant ID not found");
const { {{snakeCase name}}_id } = this.req.params as { {{snakeCase name}}_id: string };
const result = await this.useCase.execute({ {{snakeCase name}}_id, companyId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,6 @@
// Barrel de controllers
export * from "./create-{{kebabCase name}}.controller";
export * from "./delete-{{kebabCase name}}.controller";
export * from "./get-{{kebabCase name}}.controller";
export * from "./list-{{kebabCase name}}.controller";
export * from "./update-{{kebabCase name}}.controller";

View File

@ -0,0 +1,28 @@
import { ExpressController, authGuard, tenantGuard, forbidQueryFieldGuard } from "@erp/core/api";
import { List{{pascalCase name}}UseCase } from "../../../application";
/**
* Controlador de listado paginado por tenant
*/
export class List{{pascalCase name}}Controller extends ExpressController {
public constructor(private readonly useCase: List{{pascalCase name}}UseCase) {
super();
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) return this.forbiddenError("Tenant ID not found");
// TODO: mapear query → criteria/pagination
const criteria = this.req.query ?? {};
const pagination = undefined;
const result = await this.useCase.execute({ companyId, criteria, pagination });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,32 @@
import { ExpressController, authGuard, tenantGuard, forbidQueryFieldGuard } from "@erp/core/api";
import { Update{{pascalCase name}}UseCase } from "../../../application";
/**
* Controlador de actualización parcial/total
*/
export class Update{{pascalCase name}}Controller extends ExpressController {
public constructor(private readonly useCase: Update{{pascalCase name}}UseCase) {
super();
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) return this.forbiddenError("Tenant ID not found");
const { {{snakeCase name}}_id } = this.req.params as { {{snakeCase name}}_id: string };
// TODO: descomentar cuando exista DTO
// const validation = validateRequest(Update{{pascalCase name}}Dto, this.req.body);
// if (validation.isErr()) return this.badRequestError(validation.error.message);
// const dto = validation.value;
const dto = this.req.body;
const result = await this.useCase.execute({ {{snakeCase name}}_id, companyId, ...dto });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,3 @@
// Barrel de express
export * from "./controllers";
export * from "./{{kebabCase plural}}.routes";

View File

@ -0,0 +1,33 @@
import { Router } from "express";
import type { ModuleParams } from "@erp/core/api";
import { build{{pascalCase name}}Dependencies } from "../repositories";
import {
Create{{pascalCase name}}Controller,
Delete{{pascalCase name}}Controller,
Get{{pascalCase name}}Controller,
List{{pascalCase name}}Controller,
Update{{pascalCase name}}Controller,
} from "./controllers";
/**
* Monta las rutas del módulo en `params.app` bajo `params.baseRoutePath/{{kebabCase plural}}`
* @remarks
* - Las validaciones DTO están comentadas hasta que existan en `common/dto`.
*/
export const mount{{pascalCase plural}}Routes = (params: ModuleParams) => {
const router = Router();
const deps = build{{pascalCase name}}Dependencies(params);
// TODO: descomentar cuando exista validateRequest y DTOs
// const validateCreate = validateRequest(Create{{pascalCase name}}Dto);
// const validateUpdate = validateRequest(Update{{pascalCase name}}Dto);
router.get("/", (req, res, next) => deps.build.list().bind(req, res, next));
router.get("/:{{snakeCase name}}_id", (req, res, next) => deps.build.get().bind(req, res, next));
router.post("/", /* validateCreate, */ (req, res, next) => deps.build.create().bind(req, res, next));
router.patch("/:{{snakeCase name}}_id", /* validateUpdate, */ (req, res, next) => deps.build.update().bind(req, res, next));
router.put("/:{{snakeCase name}}_id", /* validateUpdate, */ (req, res, next) => deps.build.update().bind(req, res, next));
router.delete("/:{{snakeCase name}}_id", (req, res, next) => deps.build.delete().bind(req, res, next));
params.app.use(`${params.baseRoutePath}/{{kebabCase plural}}`, router);
};

View File

@ -0,0 +1,5 @@
// Barrel de infraestructura
export * from "./express";
export * from "./mappers";
export * from "./repositories";
export * from "./sequelize";

View File

@ -0,0 +1 @@
export * from "./{{kebabCase name}}.mapper";

View File

@ -0,0 +1,27 @@
import type { {{pascalCase name}} } from "../../../domain";
import type { {{pascalCase name}}Model } from "../../sequelize/models/{{kebabCase name}}.model";
/**
* Mapper de dominio ↔ ORM/DTO
* @remarks
* - SSOT: punto único de traducción entre capas.
*/
export const {{camelCase name}}DomainMapper = {
toPersistence(entity: {{pascalCase name}}) {
// TODO: mapear campos reales
return {
id: entity.id,
company_id: entity.companyId,
status: (entity as any).status ?? "active",
};
},
toDomain(model: {{pascalCase name}}Model): {{pascalCase name}} {
// TODO: mapear campos reales
return {
id: model.id,
companyId: model.company_id,
status: (model as any).status,
} as unknown as {{pascalCase name}};
},
};

View File

@ -0,0 +1,2 @@
export * from "./domain";
export * from "./queries";

View File

@ -0,0 +1 @@
export * from "./{{kebabCase name}}.list.mapper";

View File

@ -0,0 +1,13 @@
/**
* Mapper para listados (read-models)
*/
export const {{camelCase name}}ListMapper = {
toListItem(p: any) {
// TODO: adaptar a la proyección real
return {
id: p.id,
name: p.name ?? "",
status: p.status ?? "active",
};
},
};

View File

@ -0,0 +1,43 @@
import { InMemoryMapperRegistry, InMemoryPresenterRegistry, SequelizeTransactionManager } from "@erp/core/api";
import type { ModuleParams } from "@erp/core/api";
import { {{pascalCase name}}Repository } from "./{{kebabCase name}}.repository";
import { Create{{pascalCase name}}Controller, Delete{{pascalCase name}}Controller, Get{{pascalCase name}}Controller, List{{pascalCase name}}Controller, Update{{pascalCase name}}Controller } from "../express/controllers";
// Los use-cases/servicios se resuelven desde application/domain (puede no compilar hasta que existan)
import {
Create{{pascalCase name}}UseCase,
Delete{{pascalCase name}}UseCase,
Get{{pascalCase name}}UseCase,
List{{pascalCase name}}UseCase,
Update{{pascalCase name}}UseCase,
} from "../../application";
import { {{pascalCase name}}Service } from "../../domain";
/**
* Wiring/DI de infraestructura del módulo
*/
export const build{{pascalCase name}}Dependencies = (params: ModuleParams) => {
const mapperRegistry = new InMemoryMapperRegistry();
const presenterRegistry = new InMemoryPresenterRegistry();
const transactionManager = new SequelizeTransactionManager(params.database);
const repository = new {{pascalCase name}}Repository(params.database);
const service = new {{pascalCase name}}Service(repository);
// TODO: enlazar presenters reales cuando existan
// presenterRegistry.register("{{camelCase name}}.created", ...);
return {
mapperRegistry,
presenterRegistry,
transactionManager,
repository,
service,
build: {
list: () => new List{{pascalCase name}}Controller(new List{{pascalCase name}}UseCase(service)),
get: () => new Get{{pascalCase name}}Controller(new Get{{pascalCase name}}UseCase(service)),
create: () => new Create{{pascalCase name}}Controller(new Create{{pascalCase name}}UseCase(service)),
update: () => new Update{{pascalCase name}}Controller(new Update{{pascalCase name}}UseCase(service)),
delete: () => new Delete{{pascalCase name}}Controller(new Delete{{pascalCase name}}UseCase(service)),
},
};
};

View File

@ -0,0 +1,2 @@
export * from "./{{kebabCase name}}.repository";
export * from "./dependencies";

View File

@ -0,0 +1,36 @@
import type { Sequelize } from "sequelize";
import { Result } from "@erp/core/api";
import type { Criteria, Pagination } from "@erp/core/api";
import type { I{{pascalCase name}}Repository, {{pascalCase name}} } from "../../domain";
import type { {{pascalCase name}}Model } from "../sequelize/models/{{kebabCase name}}.model";
/**
* Repositorio concreto (Sequelize)
* @remarks
* - Implementa métodos por tenant (companyId) desde el inicio.
*/
export class {{pascalCase name}}Repository implements I{{pascalCase name}}Repository {
public constructor(private readonly database: Sequelize) {}
async create(entity: {{pascalCase name}}) {
// TODO: usar mapper y modelo
return Result.fail<{{pascalCase name}}>(new Error("Not implemented"));
}
async update(entity: {{pascalCase name}}) {
return Result.fail<{{pascalCase name}}>(new Error("Not implemented"));
}
async deleteByIdInCompany(id: string, companyId: string) {
return Result.fail<void>(new Error("Not implemented"));
}
async findByIdInCompany(id: string, companyId: string) {
return Result.fail<{{pascalCase name}}>(new Error("Not implemented"));
}
async findByCriteriaInCompany(criteria: Criteria, companyId: string, pagination?: Pagination) {
// TODO: aplicar where/order/limit/offset usando CriteriaToSequelizeConverter si existe en core
return Result.fail<{ items: {{pascalCase name}}[]; total: number }>(new Error("Not implemented"));
}
}

View File

@ -0,0 +1,5 @@
// Barrel de sequelize y registro de modelos
export * from "./models";
import { init{{pascalCase name}}Model } from "./models";
export const models = [init{{pascalCase name}}Model];

View File

@ -0,0 +1,2 @@
export { {{pascalCase name}}Model as {{pascalCase name}}Model } from "./{{kebabCase name}}.model";
export { default as init{{pascalCase name}}Model } from "./{{kebabCase name}}.model";

View File

@ -0,0 +1,64 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Sequelize,
} from "sequelize";
export type {{pascalCase name}}CreationAttributes = InferCreationAttributes<{{pascalCase name}}Model, {}> & {};
/**
* Modelo Sequelize (infraestructura, no dominio)
*/
export class {{pascalCase name}}Model extends Model<
InferAttributes<{{pascalCase name}}Model>,
InferCreationAttributes<{{pascalCase name}}Model>
> {
declare id: string;
declare company_id: string;
declare status: CreationOptional<string>;
declare created_at: CreationOptional<Date>;
declare updated_at: CreationOptional<Date>;
declare deleted_at: CreationOptional<Date | null>;
static associate(database: Sequelize) {
// TODO: definir relaciones si aplica
}
static hooks(database: Sequelize) {
// TODO: definir hooks si aplica
}
}
export default (database: Sequelize) => {
{{pascalCase name}}Model.init(
{
id: { type: DataTypes.UUID, primaryKey: true },
company_id: { type: DataTypes.UUID, allowNull: false },
status: { type: DataTypes.STRING, allowNull: false, defaultValue: "active" },
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
deleted_at: { type: DataTypes.DATE, allowNull: true, defaultValue: null },
},
{
sequelize: database,
tableName: "{{snakeCase plural}}",
underscored: true,
paranoid: true,
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{ name: "company_idx", fields: ["company_id"], unique: false },
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true },
],
whereMergeStrategy: "and",
defaultScope: {},
scopes: {},
}
);
return {{pascalCase name}}Model;
};

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"]
}