first commit
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
En este documento de texto voy a indicarte como quiero que procedas para crear nuevas funcionalidades siguiendo
|
||||
el estilo de programación del proyecto.
|
||||
|
||||
Te voy a detallas los pasos a seguir para crear nuevas funcionalidades. Para ello voy a darte archivos de ejemplo
|
||||
que quiero que utilices como ejemplo para crear las nuevas funciones que te pida.
|
||||
|
||||
Primero te voy a describir la estructura que estoy manejando:
|
||||
|
||||
La carpeta principal del proyecto es turnosxpress para una funcionalidad de ejemplo. En este caso vamos a ver como
|
||||
quiero que desarrolles futuras funcionalidades basandonos en la funcion existente llamada Payments.
|
||||
En el arbol de directorio que te voy a pasar veras que una funcionalidad se divide en dos areas. Una dentro
|
||||
de la carpeta api y otra dentro de la carpeta Models.
|
||||
|
||||
Dentro de la carpeta api debe ir un directorio con el nombre de la funcionalidad, en este caso "Payments" y
|
||||
dentro de esa carpeta dos archivos, uno llamado Payments.service.ts y otro llamado Payments.Controller.ts
|
||||
|
||||
Dentro de la carpeta Models debe ir un directorio con el nombre de la funcionalidad, en este caso "Payments"
|
||||
y dentro de esa carpeta tres archivos, en este caso Payments.Adapter.Mongoose.ts, Payments.interface.ts y Payments.ts
|
||||
|
||||
Luego de mostrarte el arbol de directorios te voy a detallar lo que quiero que contenga cada archivo en su interior.
|
||||
|
||||
ARBOL DE DIRECTORIOS
|
||||
|
||||
turnosxpress/
|
||||
server/
|
||||
src/
|
||||
api/
|
||||
Payments/
|
||||
Payments.Controller.ts
|
||||
Payments.service.ts
|
||||
Models/
|
||||
Payments/
|
||||
Payments.Adapter.Mongoose.ts
|
||||
Payments.interface.ts
|
||||
Payments.ts
|
||||
|
||||
Vamos a suponer que queremos crear una nueva funcionalidad llamada Payments que tenga las siguientes propiedades.
|
||||
|
||||
Propiedades:
|
||||
id, amount, clientId, description y createdAt
|
||||
|
||||
En este caso espero que me crees la estructura de directorios y archivos mencionada anterriormente en la seccion
|
||||
ARBOL DE DIRECTORIOS
|
||||
|
||||
|
||||
|
||||
CONTENIDO DE CADA ARCHIVO - Models/Payments.interface.ts:
|
||||
debe haber una interfaz que describa como es un pago, en este caso:
|
||||
export interface IPayment {
|
||||
id?: string;
|
||||
_id?: string;
|
||||
amount: number;
|
||||
clientId: string;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
Se debe definir un adaptador con los metodos, create, delete, find, findOne y paginate. Cada metodo
|
||||
debe tener como parametros un tipo definido previamente llamado con el nombre del metodo - nombre de la
|
||||
funcionalidad - terminado en Params. Por ejemplo para el metodo create el tipo debe llamarse CreatePaymentsParams
|
||||
para el metodo find, el tipo debe llamarse FindPaymentsParams y asi sucesivamente.
|
||||
Tambien nota que el metodo find devuelve un array con la interfaz definida anteriormente y el metodo findOne
|
||||
devuelve un solo elemento del tipo IPaymentDocument, que estara definido en el archivo Models/Payments.Adapter.Mongoose.ts.
|
||||
|
||||
export interface IPaymentsAdapter {
|
||||
create(data: CreatePaymentExtraParams): Promise<IPayment>;
|
||||
delete(data: DeletePaymentParams): Promise<void>;
|
||||
find(filters: FindPaymentsParams): Promise<IPayment[]>;
|
||||
findOne(filters: FindPaymentsParams): Promise<IPaymentDocument | null>;
|
||||
paginate(filters: PaginatePaymentsParams): Promise<PaginatePaymentsResults>;
|
||||
}
|
||||
|
||||
entonces los tipos que se deben generar serian:
|
||||
|
||||
export type FindPaymentsParams = {
|
||||
id?: string;
|
||||
clientId?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type PaginatePaymentsParams = FindPaymentsParams & {
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export type PaginatePaymentsResults = {
|
||||
data: IPayment[];
|
||||
page: number;
|
||||
pages: number;
|
||||
};
|
||||
|
||||
export type CreatePaymentParams = {
|
||||
amount: number;
|
||||
clientId: string;
|
||||
description?: string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type DeletePaymentParams = {
|
||||
id: string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
Tambien vamos a definir un Manager:
|
||||
|
||||
export interface IPaymentsManager {
|
||||
payments: IPaymentsAdapter;
|
||||
createPayment(data: CreatePaymentParams): Promise<IPayment>;
|
||||
deletePayment(data: DeletePaymentParams): Promise<void>;
|
||||
paginate(data: PaginatePaymentsViewParams): Promise<PaginatePaymentsViewResults>;
|
||||
}
|
||||
|
||||
CONTENIDO DE CADA ARCHIVO - Models/Payments.Adapter.Mongoose.ts:
|
||||
|
||||
Aqui se implementa el adaptador y se define como es un Documento.
|
||||
|
||||
export interface IPaymentDocument extends Omit<IPayment, "id" | "_id">, Document {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export class PaymentsAdapterMongoose implements IPaymentsAdapter {
|
||||
schema: Schema;
|
||||
paymentsList: Model<IPaymentDocument>;
|
||||
|
||||
constructor() {
|
||||
this.schema = new Schema({
|
||||
clientId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: "Client",
|
||||
},
|
||||
amount: { type: Number, required: true, default: 0 },
|
||||
description: { type: String, required: false, default: "" },
|
||||
createdAt: { type: Date, required: true, default: Date.now },
|
||||
});
|
||||
|
||||
this.paymentsList = model<IPaymentDocument>("Payment", this.schema);
|
||||
}
|
||||
|
||||
public async create(data: CreatePaymentExtraParams): Promise<IPayment> {
|
||||
return await this.paymentsList.create(data);
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
await this.paymentsList.deleteOne({ _id: id });
|
||||
}
|
||||
|
||||
public async find(filters: Omit<FindPaymentsParams, "sessionUser">): Promise<IPayment[]> {
|
||||
const searchCriteria = this.buildSearchCriteria(filters);
|
||||
return await this.paymentsList.find(searchCriteria).exec();
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
filters: Omit<FindPaymentsParams, "sessionUser">
|
||||
): Promise<IPaymentDocument | null> {
|
||||
return await this.paymentsList.findOne(filters).exec();
|
||||
}
|
||||
|
||||
private buildSearchCriteria(
|
||||
filters: Omit<FindPaymentsParams, "sessionUser">
|
||||
): FilterQuery<IPayment> {
|
||||
const searchCriteria: FilterQuery<IPayment> = {};
|
||||
|
||||
if (filters.startDate) {
|
||||
searchCriteria.createdAt = {
|
||||
$gte: filters.startDate,
|
||||
$lte: filters.endDate,
|
||||
};
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
searchCriteria.status = filters.status;
|
||||
}
|
||||
|
||||
if (filters.clientId) {
|
||||
searchCriteria.clientId = filters.clientId;
|
||||
}
|
||||
|
||||
if (filters.method) {
|
||||
searchCriteria.method = filters.method;
|
||||
}
|
||||
|
||||
if (filters.companyId) {
|
||||
searchCriteria.companyId = filters.companyId;
|
||||
}
|
||||
|
||||
if (filters.employeeId) {
|
||||
searchCriteria.employeeId = filters.employeeId;
|
||||
}
|
||||
|
||||
if (filters.serviceId) {
|
||||
searchCriteria.serviceId = filters.serviceId;
|
||||
}
|
||||
|
||||
if (filters.appointmentId) {
|
||||
searchCriteria.appointmentId = filters.appointmentId;
|
||||
}
|
||||
|
||||
return searchCriteria;
|
||||
}
|
||||
|
||||
public async paginate(filters: PaginatePaymentsParams): Promise<PaginatePaymentsResults> {
|
||||
const searchCriteria = this.buildSearchCriteria(filters);
|
||||
|
||||
const count = await this.paymentsList.countDocuments(searchCriteria).exec();
|
||||
const skip = (filters.page - 1) * filters.limit;
|
||||
|
||||
const results = await this.paymentsList
|
||||
.find(searchCriteria)
|
||||
.skip(skip)
|
||||
.limit(filters.limit)
|
||||
.sort({ createdAt: -1 })
|
||||
.exec();
|
||||
|
||||
return {
|
||||
data: results,
|
||||
page: filters.page,
|
||||
pages: Math.ceil(count / filters.limit),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
CONTENIDO DE CADA ARCHIVO - Models/Payments.ts:
|
||||
|
||||
En este archivo dejariamos el esqueleto de todas las funcionalidades definidas en el Manager creados
|
||||
pero sin contenido para ser definido posteriormente por mi.
|
||||
|
||||
CONTENIDO DE CADA ARCHIVO - api/Payments/Payments.Service.ts: ver archivo para tener el ejemplo concreto,
|
||||
pero basicamente creamos las llamadas de todos los metodos definidos en el Manager.
|
||||
|
||||
CONTENIDO DE CADA ARCHIVO - api/Payments/Payments.Controller.ts: ver archivo para tener el ejemplo concreto,
|
||||
Se deben crear los enpoints de todos los metodos accesibles por el arhivo terminado en .Services, tendiendo
|
||||
en cuenta que se debe utilizar el middleware @Middlewares(authenticateMiddleware) y que siempre usamos el metodo Post.
|
||||
|
||||
Asi deberia verse:
|
||||
@Route("payments/delete")
|
||||
@Middlewares(authenticateMiddleware)
|
||||
export class DeletePaymentController extends Controller {
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post()
|
||||
public async deletePayment(
|
||||
@Body() requestBody: DeletePaymentParams
|
||||
): Promise<ApiVoidResult | ApiValidationError> {
|
||||
try {
|
||||
await new PaymentsService().deletePayment(requestBody);
|
||||
this.setStatus(200);
|
||||
return {
|
||||
success: true,
|
||||
message: "",
|
||||
};
|
||||
} catch (e) {
|
||||
const errorOccurred: Error = e as Error;
|
||||
this.setStatus(500);
|
||||
return new ApiValidationError(500, errorOccurred.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notar que si devuelve void tenemos definido el tipo ApiVoidResult y si no devolvera el tipo definido en el archivo
|
||||
Payments.interface.ts
|
||||
|
||||
Por favor recuerda guiarte estrictamente por el ejemplo proporcionado y no te inventes nada nuevo.
|
||||
|
||||
Entonces, para entendernos mejor: Si te paso este propmpt, "Quiero generar una nueva funcionalidad llamada Discounts,
|
||||
con las propiedades: id, companyId, name, code, value, type, createdAt, donde type sera un enum con los elementos PERCENTAJE y FIXED" deberas generarme lo siguiente:
|
||||
|
||||
File: turnosxpress/server/src/Models/Discounts/Discounts.interface.ts
|
||||
Contenido:
|
||||
import { Document } from "mongoose";
|
||||
|
||||
export enum DiscountType {
|
||||
FIXED = "fixed",
|
||||
PERCENTAGE = "percentage",
|
||||
}
|
||||
|
||||
export interface IDiscount {
|
||||
id?: string;
|
||||
_id?: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: DiscountType;
|
||||
value: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IDiscountDocument extends Omit<IDiscount, "id" | "_id">, Document {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface IDiscountsAdapter {
|
||||
create(data: Omit<CreateDiscountParams, "sessionUser">): Promise<IDiscount>;
|
||||
delete(data: Omit<DeleteDiscountParams, "sessionUser">): Promise<void>;
|
||||
find(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscount[]>;
|
||||
findOne(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscountDocument | null>;
|
||||
paginate(filters: PaginateDiscountsParams): Promise<PaginateDiscountsResults>;
|
||||
}
|
||||
|
||||
export type FindDiscountsParams = {
|
||||
id?: string;
|
||||
companyId: string;
|
||||
code?: string;
|
||||
type?: DiscountType;
|
||||
sessionUser: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
startValue?: number;
|
||||
endValue?: number;
|
||||
};
|
||||
|
||||
export type PaginateDiscountsParams = FindDiscountsParams & {
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export type PaginateDiscountsResults = {
|
||||
data: IDiscount[];
|
||||
page: number;
|
||||
pages: number;
|
||||
};
|
||||
|
||||
export type CreateDiscountParams = {
|
||||
companyId: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: DiscountType;
|
||||
value: number;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type DeleteDiscountParams = {
|
||||
id: string;
|
||||
companyId: string;
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export interface IDiscountsManager {
|
||||
discounts: IDiscountsAdapter;
|
||||
createDiscount(data: CreateDiscountParams): Promise<IDiscount>;
|
||||
deleteDiscount(data: DeleteDiscountParams): Promise<void>;
|
||||
paginate(data: PaginateDiscountsParams): Promise<PaginateDiscountsResults>;
|
||||
}
|
||||
|
||||
|
||||
File: turnosxpress/server/src/Models/Discounts/Discounts.Adapter.Mongoose.ts
|
||||
Contenido:
|
||||
import {
|
||||
IDiscount,
|
||||
IDiscountDocument,
|
||||
IDiscountsAdapter,
|
||||
DiscountType,
|
||||
CreateDiscountParams,
|
||||
DeleteDiscountParams,
|
||||
FindDiscountsParams,
|
||||
PaginateDiscountsParams,
|
||||
PaginateDiscountsResults,
|
||||
} from "./Discounts.interface";
|
||||
import { Schema, model, Model, FilterQuery } from "mongoose";
|
||||
|
||||
export class DiscountsAdapterMongoose implements IDiscountsAdapter {
|
||||
schema: Schema;
|
||||
discountsList: Model<IDiscountDocument>;
|
||||
|
||||
constructor() {
|
||||
this.schema = new Schema({
|
||||
companyId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: "Company",
|
||||
},
|
||||
name: { type: String, required: true },
|
||||
code: { type: String, required: true },
|
||||
type: { type: String, enum: Object.values(DiscountType), required: true },
|
||||
value: { type: Number, required: true },
|
||||
createdAt: { type: Date, default: Date.now, required: true },
|
||||
});
|
||||
|
||||
this.discountsList = model<IDiscountDocument>("Discount", this.schema);
|
||||
}
|
||||
|
||||
public async create(data: Omit<CreateDiscountParams, "sessionUser">): Promise<IDiscount> {
|
||||
return await this.discountsList.create(data);
|
||||
}
|
||||
|
||||
public async delete(data: Omit<DeleteDiscountParams, "sessionUser">): Promise<void> {
|
||||
await this.discountsList.deleteOne({ _id: data.id });
|
||||
}
|
||||
|
||||
public async find(filters: Omit<FindDiscountsParams, "sessionUser">): Promise<IDiscount[]> {
|
||||
const searchCriteria = this.buildSearchCriteria(filters);
|
||||
return await this.discountsList.find(searchCriteria).exec();
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
filters: Omit<FindDiscountsParams, "sessionUser">
|
||||
): Promise<IDiscountDocument | null> {
|
||||
const searchCriteria = this.buildSearchCriteria(filters);
|
||||
return await this.discountsList.findOne(searchCriteria).exec();
|
||||
}
|
||||
|
||||
private buildSearchCriteria(
|
||||
filters: Omit<FindDiscountsParams, "sessionUser">
|
||||
): FilterQuery<IDiscount> {
|
||||
const searchCriteria: FilterQuery<IDiscount> = {};
|
||||
|
||||
if (filters.id) searchCriteria._id = filters.id;
|
||||
if (filters.companyId) searchCriteria.companyId = filters.companyId;
|
||||
if (filters.code) searchCriteria.code = { $regex: filters.code, $options: "i" };
|
||||
if (filters.type) searchCriteria.type = filters.type;
|
||||
|
||||
if (filters.startDate && filters.endDate) {
|
||||
searchCriteria.createdAt = {
|
||||
$gte: filters.startDate,
|
||||
$lte: filters.endDate,
|
||||
};
|
||||
}
|
||||
|
||||
if (filters.startValue !== undefined && filters.endValue !== undefined) {
|
||||
searchCriteria.value = {
|
||||
$gte: filters.startValue,
|
||||
$lte: filters.endValue,
|
||||
};
|
||||
}
|
||||
|
||||
return searchCriteria;
|
||||
}
|
||||
|
||||
public async paginate(filters: PaginateDiscountsParams): Promise<PaginateDiscountsResults> {
|
||||
const searchCriteria = this.buildSearchCriteria(filters);
|
||||
|
||||
const count = await this.discountsList.countDocuments(searchCriteria).exec();
|
||||
const skip = (filters.page - 1) * filters.limit;
|
||||
|
||||
const results = await this.discountsList
|
||||
.find(searchCriteria)
|
||||
.skip(skip)
|
||||
.limit(filters.limit)
|
||||
.sort({ createdAt: -1 })
|
||||
.exec();
|
||||
|
||||
return { data: results, page: filters.page, pages: Math.ceil(count / filters.limit) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File: turnosxpress/server/src/Models/Discounts/Discounts.ts
|
||||
Contenido:
|
||||
import { validatePermissionsByCompany, validateSessionUser } from "src/helpers/check";
|
||||
import {
|
||||
IDiscountsManager,
|
||||
IDiscountsAdapter,
|
||||
CreateDiscountParams,
|
||||
DeleteDiscountParams,
|
||||
IDiscount,
|
||||
PaginateDiscountsParams,
|
||||
PaginateDiscountsResults,
|
||||
} from "./Discounts.interface";
|
||||
|
||||
export class DiscountsManager implements IDiscountsManager {
|
||||
discounts: IDiscountsAdapter;
|
||||
|
||||
constructor(discountsAdapter: IDiscountsAdapter) {
|
||||
this.discounts = discountsAdapter;
|
||||
}
|
||||
|
||||
async createDiscount(data: CreateDiscountParams): Promise<IDiscount> {
|
||||
await validateSessionUser({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
await validatePermissionsByCompany({
|
||||
companyId: data.companyId,
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
// La lógica de negocio se implementará aquí
|
||||
const discount = this.discounts.create(data);
|
||||
|
||||
if (!discount) {
|
||||
throw new Error("Error creating discount");
|
||||
}
|
||||
|
||||
return discount;
|
||||
}
|
||||
|
||||
async deleteDiscount(data: DeleteDiscountParams): Promise<void> {
|
||||
await validateSessionUser({
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
const discount = await this.discounts.findOne({ id: data.id, companyId: data.companyId });
|
||||
|
||||
if (!discount) {
|
||||
throw new Error("El descuento que intenta eliminar no existe");
|
||||
}
|
||||
|
||||
await validatePermissionsByCompany({
|
||||
companyId: String(discount.companyId),
|
||||
sessionUser: data.sessionUser,
|
||||
});
|
||||
|
||||
await this.discounts.delete(data);
|
||||
}
|
||||
|
||||
async paginate(data: PaginateDiscountsParams): Promise<PaginateDiscountsResults> {
|
||||
return this.discounts.paginate(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File: turnosxpress/server/src/api/Discounts/Discounts.Controller.ts
|
||||
Contenido:
|
||||
import { Controller, Post, Route, SuccessResponse, Body, Middlewares } from "tsoa";
|
||||
import { DiscountsService } from "./Discounts.Service";
|
||||
import {
|
||||
CreateDiscountParams,
|
||||
DeleteDiscountParams,
|
||||
IDiscount,
|
||||
PaginateDiscountsParams,
|
||||
PaginateDiscountsResults,
|
||||
} from "../../Models/Discounts/Discounts.Interface";
|
||||
import { ApiValidationError } from "src/Models/Server.Error.model";
|
||||
import { ApiVoidResult } from "src/Models/Api.VoidResult.type";
|
||||
import { authenticateMiddleware } from "src/middleware/authentication";
|
||||
|
||||
@Route("discounts")
|
||||
export class DiscountsController extends Controller {
|
||||
@SuccessResponse(201, "Created")
|
||||
@Post("create")
|
||||
@Middlewares(authenticateMiddleware)
|
||||
public async createDiscount(
|
||||
@Body() requestBody: CreateDiscountParams
|
||||
): Promise<IDiscount | ApiValidationError> {
|
||||
try {
|
||||
const discount = await new DiscountsService().createDiscount(requestBody);
|
||||
this.setStatus(201);
|
||||
return discount;
|
||||
} catch (e) {
|
||||
const errorOccurred: Error = e as Error;
|
||||
this.setStatus(500);
|
||||
return new ApiValidationError(500, errorOccurred.message);
|
||||
}
|
||||
}
|
||||
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post("delete")
|
||||
@Middlewares(authenticateMiddleware)
|
||||
public async deleteDiscount(
|
||||
@Body() requestBody: DeleteDiscountParams
|
||||
): Promise<ApiVoidResult | ApiValidationError> {
|
||||
try {
|
||||
await new DiscountsService().deleteDiscount(requestBody);
|
||||
this.setStatus(200);
|
||||
return { success: true, message: "Discount deleted successfully." };
|
||||
} catch (e) {
|
||||
const errorOccurred: Error = e as Error;
|
||||
this.setStatus(500);
|
||||
return new ApiValidationError(500, errorOccurred.message);
|
||||
}
|
||||
}
|
||||
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post("paginate")
|
||||
@Middlewares(authenticateMiddleware)
|
||||
public async paginate(
|
||||
@Body() requestBody: PaginateDiscountsParams
|
||||
): Promise<PaginateDiscountsResults | ApiValidationError> {
|
||||
try {
|
||||
const result = await new DiscountsService().paginate(requestBody);
|
||||
this.setStatus(200);
|
||||
return result;
|
||||
} catch (e) {
|
||||
const errorOccurred: Error = e as Error;
|
||||
this.setStatus(500);
|
||||
return new ApiValidationError(500, errorOccurred.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Y asi sucesivamente dependiendo del nombre de la funcionalidad y de las propiedades que te pida.
|
||||
Reference in New Issue
Block a user