Decoradores de field-schema
Objetivo
Sección titulada «Objetivo»Elegir el decorador correcto de field-schema para el handler que acabas de scaffoldear, cablearlo en main() y dejar que él se ocupe de la validación, la sanitización y el enmascarado del output — en lugar de duplicar esas comprobaciones a mano.
Antes de empezar
Sección titulada «Antes de empezar»- Tienes un módulo backend scaffoldeado y su archivo
*.field-schema.tsexiste en disco. El CLI lo generó desdeaggregatePropertiesen la YAML del módulo; no lo edites a mano. - Sabes qué rol tiene el handler: ¿recibe un payload?, ¿devuelve la entidad?
@aurorajs.dev/core-backy@aurorajs.dev/core-commonestán instalados — ambos vienen con cualquier backend de Catalyst.
-
Elige el decorador con este árbol de decisión.
¿El handler RECIBE un payload?├── No → @Format(schema)└── Sí → ¿DEVUELVE la entidad?├── Sí → @ApplySchema(schema)└── No → @Sanitize(schema)@ApplySchemacompone sanitize (input) + format (output).@Formatsolo formatea el retorno (las lecturas son idempotentes).@Sanitizesolo sanitiza el payload (flujos de escritura cuyo retorno esvoid, un booleano o un tipo resumen). -
Cablea
@ApplySchemaen un handler de escritura que devuelve la entidad. Ejemplo real desdeiam/tag:import { ApplySchema, EmitEvent } from '@aurorajs.dev/core-back';import { IamTagFieldSchema } from '@app/iam/tag/domain';@EmitEvent('iam.tag.created')@ApplySchema(IamTagFieldSchema)async main(payload: IamCreateTagInput, handlerMeta?: HandlerMeta): Promise<IamTag> {await this.createService.main(payload, handlerMeta);return await this.findByIdService.main(payload.id, {}, handlerMeta);}sanitizecorre sobrepayloadANTES de que se ejecutemain()— eniam/user, por ejemplo, ahí es dondetype: 'password'convierte el texto plano en un hash bcrypt.formatcorre sobre el retorno DESPUÉS de quemain()resuelva — ahípasswordse vuelveundefinedantes de salir al cliente. -
Cablea
@Formaten un handler de lectura.import { Format } from '@aurorajs.dev/core-back';@Format(IamTagFieldSchema)async main(id: string, constraint?: QueryStatement, handlerMeta?: HandlerMeta): Promise<IamTag> {const tag = await this.findByIdService.main(id, constraint, handlerMeta);if (!tag) throw new NotFoundException(`IamTag with id: ${id}, not found`);return tag;}@Formatdetecta automáticamente la forma del retorno — objeto individual, array oPaginationcon una lista.rows— y formatea cada elemento. -
Si el payload no es el primer argumento, usa la forma de objeto.
@ApplySchema({ schema: IamTagFieldSchema, payloadIndex: 1 })async main(constraint, payload, handlerMeta?) { … }Misma opción en
@Sanitize.@Formatno tienepayloadIndex— siempre opera sobre el retorno. -
En handlers de update, calcula el delta con
Obj.diff. Tras la sanitización, persiste solo lo que cambió. Uso real eniam-update-tag-by-id.handler.ts:import { Obj } from '@aurorajs.dev/core-common';const tag = await this.findByIdService.main(payload.id, constraint, handlerMeta);if (!tag) throw new NotFoundException(`IamTag with id: ${payload.id}, not found`);const dataToUpdate = Obj.diff(payload, tag);await this.updateByIdService.main({ ...dataToUpdate, id: payload.id }, // reinyecta el id — diff omite claves coincidentesconstraint,handlerMeta,);Obj.diffomite las claves cuyo valor coincide en ambos lados, así que elidhay que volver a añadirlo explícitamente como clave objetivo.
Verifica que funcionó
Sección titulada «Verifica que funcionó»- Para un campo
type: 'password', crea un usuario e inspecciona la fila: la columna guarda un hash bcrypt, nunca texto plano. Vuelve a leerlo — la clavepasswordno aparece en la respuesta. - Para
maxLength,enumOptionsonullable, envía un payload que viole la restricción. El decorador lanza antes de quemain()se ejecute. - Los timestamps del retorno llevan la timezone del caller. Esa es la señal de que
@Format/@ApplySchemaextrajohandlerMeta.timezone— una llamada directa aformatRecord()la perdería.
Solución de problemas
Sección titulada «Solución de problemas»Una relación cargada con include filtra un campo sensible. formatRecord no entra recursivamente en las relaciones cargadas con include. Ese caso se trata aparte — mira Componer un QueryStatement.
La validación corre dos veces — una en el decorador y otra dentro de main(). Elimina la comprobación del handler. @ApplySchema ya aplicó maxLength, nullable, enumOptions y las rules declaradas. Deja solo reglas de negocio que la FieldSchema no pueda expresar (invariantes entre campos, cálculos de dominio).
Recurres a un Value Object para encapsular la validación. Catalyst no tiene capa de VO — la validación y la modificación se delegan completamente a FieldSchema + type handlers vía @ApplySchema. Expresa la regla en aggregateProperties de la YAML y regenera.
Llamada directa a formatRecord() o sanitizeRecord(). Evítala. El decorador cablea handlerMeta.timezone y otras opciones automáticamente; una llamada directa pierde ese contexto y produce output inconsistente.
Relacionado
Sección titulada «Relacionado»- Scaffolding de un módulo backend — por qué
*.field-schema.tsse genera desde YAML y no se edita a mano. - Componer un QueryStatement — el lado de consulta de la capa declarativa de acceso a datos.