Last active
July 27, 2025 14:11
-
-
Save VigilioYonatan/2c116be9801622528d58d8bbbcf29402 to your computer and use it in GitHub Desktop.
optimizacion good practices.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Se agrega un campo mas en los DELETEAT, y a sus childs isDelete | |
// DELETE CONSUME MUCHA MEMORIA, usar DELETE una vez cada 24 horas | |
const { User, Post, Comment, Like } = require('../models'); | |
class UserService { | |
async softDeleteUser(userId) { | |
const transaction = await sequelize.transaction(); | |
try { | |
// 1. Marcar usuario como eliminado | |
await User.update( | |
{ isDeleted: true, deletedAt: new Date() }, | |
{ where: { id: userId }, transaction } | |
); | |
// 2. Marcar posts del usuario como eliminados | |
await Post.update( | |
{ isDeleted: true }, | |
{ where: { userId }, transaction } | |
); | |
// 3. Marcar comentarios del usuario como eliminados | |
await Comment.update( | |
{ isDeleted: true }, | |
{ where: { userId }, transaction } | |
); | |
// 4. Eliminar likes del usuario (relación directa sin soft delete), los likes son campos con pocas tablas, no llegan a tener imagenes, etc pesados | |
await Like.destroy({ | |
where: { userId }, | |
transaction | |
}); | |
await transaction.commit(); | |
// Programar limpieza física para después | |
setTimeout(() => this.physicalCleanup(userId), 24 * 60 * 60 * 1000); // 24 horas después o poner la fecha que deseas -. menos de 1 mes recomendable. | |
return { success: true, message: 'Usuario marcado para eliminación' }; | |
} catch (error) { | |
await transaction.rollback(); | |
throw error; | |
} | |
} | |
async physicalCleanup(userId) { | |
const transaction = await sequelize.transaction(); | |
try { | |
// Eliminar en lotes para evitar bloqueos | |
let hasMoreComments = true; | |
while (hasMoreComments) { | |
const result = await Comment.destroy({ | |
where: { userId, isDeleted: true }, | |
limit: 100, | |
transaction | |
}); | |
hasMoreComments = result === 100; | |
} | |
let hasMorePosts = true; | |
while (hasMorePosts) { | |
const result = await Post.destroy({ | |
where: { userId, isDeleted: true }, | |
limit: 100, | |
transaction | |
}); | |
hasMorePosts = result === 100; | |
} | |
// Finalmente eliminar el usuario | |
await User.destroy({ | |
where: { id: userId, isDeleted: true }, | |
transaction | |
}); | |
await transaction.commit(); | |
} catch (error) { | |
await transaction.rollback(); | |
// Reintentar más tarde | |
setTimeout(() => this.physicalCleanup(userId), 60 * 60 * 1000); // Reintentar en 1 hora | |
} | |
} | |
} | |
module.exports = new UserService(); | |
// Claro CUANDO VAS A MOSTRAR usa User.finAdll({where:{isDelete:null}) al igual que a sus hijos |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
Esto sirve para no consumir muchos recursos a la hora de insertar muchos datos | |
*/ | |
export async function bulkCreateWithNestedRelations< | |
T extends object, | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
R extends RelationConfig<any, any>[] = RelationConfig<any, any>[] | |
>( | |
mainConfig: BulkCreateConfig<T>, | |
relations: R, | |
options: { transaction?: Transaction } = {} | |
): Promise<Array<T & { id: number }>> { | |
const transaction = options.transaction; | |
const chunkSize = mainConfig.chunkSize || 1000; | |
const processInChunks = async <D extends object>( | |
model: typeof Model<D, D>, | |
data: D[], | |
config?: Omit<BulkCreateConfig<D>, "data" | "model" | "transaction"> | |
): Promise<Array<D & { id: number }>> => { | |
const results: Array<D & { id: number }> = []; | |
for (let i = 0; i < data.length; i += chunkSize) { | |
const chunk = data.slice(i, i + chunkSize); | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
const created = await (model as any).bulkCreate(chunk, { | |
transaction, | |
returning: true, | |
...config, | |
}); | |
results.push(...created); | |
} | |
return results; | |
}; | |
const createLevel = async <P extends object, C extends object>( | |
parentData: P[], | |
parentResults: Array<P & { id: number }>, | |
relationConfig: RelationConfig<P, C>, | |
childConfig: BulkCreateConfig<C> | |
): Promise<Array<C & { id: number }>> => { | |
const childrenToCreate: C[] = []; | |
for (const [i, parentItem] of parentData.entries()) { | |
const parentResult = parentResults[i]; | |
const children = | |
(parentItem[relationConfig.childrenField] as unknown as C[]) || | |
[]; | |
for (const child of children) { | |
let childData = { ...child }; | |
if (childConfig.excludeFields) { | |
for (const field of childConfig.excludeFields) { | |
delete childData[field as keyof C]; | |
} | |
} | |
childData[relationConfig.relationField] = | |
parentResult.id as never; | |
if (childConfig.beforeCreate) { | |
childData = childConfig.beforeCreate( | |
childData as Omit<C, "id"> | |
) as C; | |
} | |
childrenToCreate.push(childData); | |
} | |
} | |
if (childrenToCreate.length > 0) { | |
return processInChunks( | |
childConfig.model, | |
childrenToCreate, | |
childConfig | |
); | |
} | |
return []; | |
}; | |
// Process main data | |
const mainData = mainConfig.data.map((item) => { | |
const itemData = { ...item }; | |
if (mainConfig.excludeFields) { | |
for (const field of mainConfig.excludeFields) { | |
delete itemData[field as keyof Omit<T, "id">]; | |
} | |
} | |
return mainConfig.beforeCreate | |
? mainConfig.beforeCreate(itemData) | |
: itemData; | |
}); | |
const mainResults = await processInChunks( | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
(mainConfig as any).model, | |
mainData | |
); | |
// Process nested relations | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
let currentParents: any[] = mainConfig.data; | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
let currentResults: any[] = mainResults; | |
for (const relation of relations) { | |
const childResults = await createLevel( | |
currentParents, | |
currentResults, | |
relation, | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
relation.config as BulkCreateConfig<any> | |
); | |
// Update for next level | |
currentParents = currentParents.flatMap( | |
(parent) => | |
(parent[relation.childrenField as keyof T] as object[]) || [] | |
) as T[]; | |
currentResults = childResults; | |
} | |
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | |
return mainResults as any; | |
} | |
// USO, ADIOS promise.all() | |
body.pages = [{ | |
id:1, | |
...rest, | |
gridbanners:[ | |
{ | |
id:1, | |
...rest, | |
gridBanners_items:[{...rest}] | |
} | |
] | |
}] | |
const createdPages = await bulkCreateWithNestedRelations( | |
{ | |
model: Page, | |
data: body.pages, | |
excludeFields: ['id', 'createdAt', 'updatedAt'], // quito id, etc por que eso no es necesario | |
beforeCreate: (page) => ({ | |
...page, | |
empresa_id: empresa!.id // si quiero persalizar el inserto | |
}), | |
chunkSize: 1000 // 500 - 5000 para que no enviar milloiens en solo bulk para no consumir mucha memoria | |
}, | |
[ // si tienes mas hijos etc | |
{ | |
relationField: 'page_id', | |
childrenField: 'grid_banners', | |
config: { | |
model: GridBanner, | |
excludeFields: ['id', 'createdAt', 'updatedAt'], | |
beforeCreate: (banner) => ({ | |
...banner, | |
empresa_id: empresa!.id | |
}) | |
} | |
}, | |
{ | |
relationField: 'grid_banner_id', | |
childrenField: 'banner_items', | |
config: { | |
model: BannerItem, | |
excludeFields: ['id', 'createdAt', 'updatedAt'] | |
} | |
} | |
] | |
); |
Comments are disabled for this gist.