services/api/src/modules/media/media.service.js
Overview
Este módulo concentra la lógica de media para la API de Somos Plano. Su foco principal es ImageKit: subida, normalización, verificación de propiedad y borrado de assets asociados a usuarios y listings. También expone utilidades pequeñas para construir rutas de carpeta y resolver URLs de imagen, thumbnail y PDF.
El archivo funciona como una capa de servicio determinista sobre ImageKit y el modelo Listing. Esto permite que el backend controle qué archivos se aceptan, dónde se guardan y cuáles pueden eliminarse al reconciliar media de un anuncio.
[!IMPORTANT] La verificación de propiedad no confía en el cliente. Cuando llega un
fileIdnuevo, el servidor consulta ImageKit y valida la carpeta real del archivo.
How it works
flowchart TD
Upload[uploadAssetToImageKit] --> Infer[inferMediaType]
Upload --> Folder[buildFolder]
Upload --> Name[sanitizeFileName]
Upload --> Client[getImageKitClient]
Upload --> Normalize[normalizeUploadResponse]
Delete[deleteAsset] --> Client2[getImageKitClient]
Delete --> NotFound[NotFoundError]
Trusted[resolveTrustedListingMedia] --> Existing[existingMedia]
Trusted --> Assert[assertOwnedListingMediaFile]
Trusted --> Resolve[known refs and fileId]
ListingDelete[deleteListingMediaAssets] --> ListingModel[Listing.findById]
ListingDelete --> AssetDelete[deleteFileIdsIndependently]
SourceDelete[deleteListingSourceDocumentAssets] --> SourceIds[collectSourceDocumentFileIds]
SourceDelete --> AssetDelete
El flujo general empieza con la subida o la reconciliación de media. Para subir, el servicio calcula el tipo de archivo, valida límites y construye una carpeta segura. Después usa el cliente de ImageKit y devuelve una respuesta normalizada para el resto del sistema.
Para borrado, el módulo reúne fileId de media y documentos fuente, y elimina cada asset de forma independiente. Si un asset ya no existe en ImageKit, el servicio lo trata como éxito para mantener idempotencia parcial.
Key components
{
"title": "sanitizeFileName",
"content": "Funci\u00f3n utilitaria [badge:public] que limpia nombres de archivo y extiende una base segura a partir del nombre original.",
"type": "function",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}
{
"title": "uploadAssetToImageKit",
"content": "Sube un archivo a ImageKit, valida tipo y tama\u00f1o, construye la carpeta destino y devuelve una respuesta normalizada.",
"type": "function",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}
{
"title": "resolveTrustedListingMedia",
"content": "Reconcilia media entrante con media ya guardada y conserva la identidad persistida cuando el servidor ya conoce el archivo.",
"type": "function",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}
{
"title": "assertOwnedListingMediaFile",
"content": "Verifica en ImageKit que un `fileId` pertenece a una carpeta v\u00e1lida del usuario o listing.",
"type": "function",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}
{
"title": "deleteListingMediaAssets",
"content": "Elimina assets de ImageKit asociados a un listing, incluyendo media del anuncio y documentos fuente.",
"type": "function",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}
{
"title": "deleteListingSourceDocumentAssets",
"content": "Elimina \u00fanicamente los `fileId` de `sourceDocuments` de un listing.",
"type": "function",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}
Data & fields
[
{
"name": "IMAGEKIT_ROOT",
"type": "string",
"required": true,
"description": "Raíz fija usada para construir carpetas de ImageKit. Su valor es `/plano`."
},
{
"name": "MEDIA_CONFIG.maxImageSizeBytes",
"type": "number",
"required": true,
"description": "Límite máximo de tamaño para imágenes. Se usa para bloquear subidas que exceden el límite."
},
{
"name": "MEDIA_CONFIG.maxImageSizeMB",
"type": "number",
"required": true,
"description": "Valor en megabytes usado en el mensaje de error cuando una imagen supera el límite."
}
]
La función normalizeUploadResponse transforma la respuesta de ImageKit en una estructura homogénea. Ese objeto incluye:
[
{
"name": "provider",
"type": "string",
"required": true,
"description": "Proveedor de media. Siempre se fija en `imagekit`."
},
{
"name": "fileId",
"type": "string",
"required": true,
"description": "Identificador del archivo en ImageKit."
},
{
"name": "name",
"type": "string",
"required": true,
"description": "Nombre visible del archivo."
},
{
"name": "fileName",
"type": "string",
"required": true,
"description": "Nombre de archivo persistido o derivado del original."
},
{
"name": "filePath",
"type": "string",
"required": true,
"description": "Ruta del archivo dentro de ImageKit."
},
{
"name": "url",
"type": "string",
"required": true,
"description": "URL pública devuelta por ImageKit."
},
{
"name": "thumbnailUrl",
"type": "string",
"required": true,
"description": "URL de thumbnail devuelta por ImageKit, si existe."
},
{
"name": "width",
"type": "number|null",
"required": true,
"description": "Ancho reportado por ImageKit."
},
{
"name": "height",
"type": "number|null",
"required": true,
"description": "Alto reportado por ImageKit."
},
{
"name": "size",
"type": "number|null",
"required": true,
"description": "Tamaño del archivo."
},
{
"name": "mime",
"type": "string",
"required": true,
"description": "Tipo MIME reportado."
},
{
"name": "mimeType",
"type": "string",
"required": true,
"description": "Alias del tipo MIME."
},
{
"name": "status",
"type": "string",
"required": true,
"description": "Estado fijo del asset. Se establece como `active`."
},
{
"name": "uploadedAt",
"type": "string",
"required": true,
"description": "Marca temporal ISO de la subida."
}
]
resolveTrustedListingMedia devuelve la colección entrante, pero reescribe campos cuando el servidor puede confiar en una referencia previa.
[
{
"name": "incomingMedia",
"type": "array",
"required": true,
"description": "Media enviada por el cliente o por el flujo de edición."
},
{
"name": "existingMedia",
"type": "array",
"description": "Media ya guardada en el listing."
},
{
"name": "userId",
"type": "string",
"required": true,
"description": "Identificador del usuario propietario o editor."
},
{
"name": "listingId",
"type": "string",
"description": "Identificador del listing para validar carpetas y ownership."
}
]
Behaviors
sanitizeSegment(value = "")— Normaliza texto, elimina acentos, convierte caracteres no seguros a guiones y produce un segmento en minúsculas.sanitizeFileName(filename = "archivo")— Limpia nombre y extensión, y devuelve un nombre de archivo seguro.buildFolder({ requestedFolder, userId, listingId })— Construye la carpeta destino de ImageKit según el tipo de media solicitado.inferMediaType(mime = "")— Clasifica el archivo comoimage,video,pdfofile.normalizeUploadResponse(response = {}, file = {})— Convierte la respuesta de ImageKit en una estructura estable para persistencia.uploadAssetToImageKit(...)— Valida, sube y normaliza un archivo.getImageKitSrc(media = {})— Resuelve la URL principal de media desdefilePath,srcourl.getCardImageUrl(media = {})— Devuelve la URL usada para imagen de tarjeta.getThumbnailUrl(media = {})— DevuelvethumbnailUrlo la URL principal.getPdfImageUrl(media = {})— Devuelve la URL principal para representación visual de PDF.deleteAsset(fileId)— Borra un asset individual o lo trata como éxito si ya no existe.deleteImageKitAsset(fileId)— Alias público dedeleteAsset.mediaError(statusCode, code, message)— Fabrica errores constatusCodeycode.allowedListingMediaPrefixes({ userId, listingId })— Construye prefijos válidos de carpeta para media de listing.assertOwnedListingMediaFile({ fileId, userId, listingId })— Comprueba ownership real contra ImageKit.resolveTrustedListingMedia({ incomingMedia, existingMedia, userId, listingId })— Reconcilia media del cliente con media persistida.collectSourceDocumentFileIds(listing)— ExtraefileIddesdelisting.sourceDocuments.deleteFileIdsIndependently(fileIds)— Borra varios assets uno por uno y agrega fallos al final.deleteListingSourceDocumentAssets(listing)— Borra solo documentos fuente del listing.deleteListingMediaAssets(listingOrId)— Borra media y documentos fuente asociados a un listing.
How the media service decides where files go
flowchart TD
Input[requestedFolder userId listingId] --> FolderCheck{folder includes}
FolderCheck -->|avatar| AvatarPath[plano users userId avatar]
FolderCheck -->|banner or cover| BannerPath[plano users userId banner]
FolderCheck -->|pdf| PdfPath[plano listings listingId or tmp userId pdfs]
FolderCheck -->|video| VideoPath[plano listings listingId or tmp userId videos]
FolderCheck -->|other| ListingPath[plano listings listingId or tmp userId]
El módulo convierte la intención de carpeta en una ruta predecible dentro de IMAGEKIT_ROOT. Esa lógica separa avatares, banners, PDFs, videos y media general de listings.
buildFolder usa sanitizeSegment para evitar caracteres problemáticos en userId y listingId. Si no hay listingId, el servicio cae en una carpeta temporal con prefijo tmp-.
Subida de assets
1. Validar entrada | `uploadAssetToImageKit` exige un archivo y rechaza la ausencia con error 400.
2. Detectar tipo | `inferMediaType` clasifica el MIME en imagen, video, PDF o archivo genérico.
3. Aplicar reglas | Las imágenes se limitan por `MEDIA_CONFIG.maxImageSizeBytes`.
4. Elegir carpeta | `buildFolder` calcula la ruta destino según `requestedFolder`, `userId` y `listingId`.
5. Limpiar nombre | `sanitizeFileName` genera un nombre seguro para ImageKit.
6. Subir a ImageKit | El cliente creado por `getImageKitClient` envía el archivo con etiquetas normalizadas.
7. Normalizar salida | `normalizeUploadResponse` devuelve un contrato estable para el resto de la API.
[!NOTE] La subida acepta solo
image,videoy
Verificación de ownership de media
assertOwnedListingMediaFile no acepta la versión pública del cliente como fuente de verdad. Primero consulta ImageKit con fileId. Luego verifica que filePath empiece por uno de los prefijos permitidos para ese usuario o listing.
Si ImageKit responde con NotFoundError, el servicio devuelve un error MEDIA_NOT_OWNED. Si falla la consulta por otro motivo, devuelve MEDIA_VERIFICATION_FAILED con estado 502.
[!WARNING] Un
fileIdde ImageKit solo es aceptado si pertenece a una carpeta válida del usuario o del listing.
Reconciliación de media de listing
resolveTrustedListingMedia protege el flujo de edición y evita que el cliente reescriba identidad de media ya conocida. El servicio crea dos índices internos: uno por referencias públicas src y url, y otro por fileId.
Cuando un ítem entrante coincide con un elemento ya guardado, el servicio reutiliza provider, fileId y filePath persistidos. Si el ítem trae un fileId nuevo de ImageKit, el servidor lo verifica y usa el filePath devuelto por ImageKit.
[!IMPORTANT] Un item histórico sin
provider: "imagekit"no se “promueve” automáticamente. Si trae unfileId, ese identificador se trata como nuevo y se verifica otra vez.
Borrado de assets
deleteAsset envuelve el borrado individual de ImageKit. Si el asset ya no existe, la función lo considera un éxito con alreadyMissing: true. Eso hace seguro repetir operaciones parciales.
deleteListingMediaAssets y deleteListingSourceDocumentAssets reutilizan deleteFileIdsIndependently. Esa función intenta cada borrado por separado y solo lanza un error agregado al final si hubo fallos.
flowchart TD
ListingInput[listing or listingId] --> Load[Listing.findById]
Load --> MediaIds[media imagekit fileId]
Load --> SourceIds[sourceDocuments fileId]
MediaIds --> Batch[deleteFileIdsIndependently]
SourceIds --> Batch
Batch --> PerFile[deleteAsset]
PerFile --> IK[ImageKit delete]
PerFile --> Missing[NotFoundError treated as success]
Batch --> Aggregate[aggregate error if failures]
Gotchas & edge cases
[!WARNING]
deleteFileIdsIndependentlyno corta el proceso al primer error. Intenta borrar todos los archivos antes de lanzar un error agregado.
[!CAUTION] Si
existingMediacontiene el mismofileIdcon rutas distintas,resolveTrustedListingMediavacíafilePathpara esefileIdpor considerarlo no confiable.
[!NOTE]
getThumbnailUrl,getCardImageUrlygetPdfImageUrlson resolutores simples. Si no hay valor específico, todas caen engetImageKitSrc.
[!WARNING]
uploadAssetToImageKitsolo permite subidas cuando existefile.buffer. El servicio trabaja con archivos recibidos en memoria.
[!IMPORTANT]
deleteListingMediaAssetsincluye media del listing ysourceDocuments. El borrado no se limita a la colecciónmedia.
Related
Este módulo se conecta con componentes ya visibles en el código proporcionado:
Listingdesde../../models/Listing.js, usado para cargar listings antes del borrado.getImageKitClientdesde../../config/imagekit.js, que crea el cliente de ImageKit.MEDIA_CONFIGdesde../../config/media.js, que aporta límites de tamaño.toFileyNotFoundErrordesde@imagekit/nodejs, usados en subida y manejo de borrado.IMAGEKIT_ROOTcomo raíz operativa de carpetas dentro de ImageKit.
Listingaporta la persistencia del anuncio.getImageKitClientconcentra la conexión con ImageKit.MEDIA_CONFIGgobierna la validación de imágenes.- ImageKit actúa como fuente de verdad para
fileIdyfilePath.
{
"title": "ImageKit integration",
"content": "Este m\u00f3dulo implementa la integraci\u00f3n operativa con ImageKit para subida, consulta y borrado de media.",
"type": "class",
"filePath": "services/api/src/modules/media/media.service.js",
"badges": [
"public"
]
}