@aventii/document-validation (1.0.0)
Installation
@aventii:registry=npm install @aventii/document-validation@1.0.0"@aventii/document-validation": "1.0.0"About this package
@aventii/document-validation
Workflow générique de soumission et de validation documentaire pour les microservices Node.js Aventii. @aventii/document-validation extrait le mécanisme commun de PMC-DI sans connaître les règles métier de Rapidoo.
Le package sait qu'un document est soumis, consulté, validé ou rejeté. Il ne sait pas ce que ce document prouve.
Installation
npm install @aventii/document-validation @aventii/files
Pour MongoDB natif : npm install mongodb. Pour le router : npm install express. Prérequis : Node.js >= 22. Le package est CommonJS.
Rôle
Le workflow V1 est :
SUBMIT
↓
PENDING
├── VALIDATE
│ ↓
│ cleanup fichier
│ ↓
│ VALIDATED
│ ↓
│ hook post-validation
│
└── REJECT
↓
cleanup fichier
↓
REJECTED
↓
hook post-rejet
Le stockage binaire est délégué à une instance compatible avec @aventii/files. La persistance est déléguée à un repository. Les conséquences métier restent dans le microservice consommateur.
API publique
const {
createDocumentValidation,
createDocumentValidationRouter,
createMongoDocumentValidationRepository,
DOCUMENT_VALIDATION_STATUS,
DocumentValidationError
} = require('@aventii/document-validation');
Architecture
microservice
/ | \
/ | \
@aventii/db @aventii/files métier
| | |
└──── repository hooks ┘
\ /
\ /
@aventii/document-validation
Le cœur ne dépend directement ni de @aventii/db, ni de MongoDB. Le microservice compose les briques et fournit au repository une fonction getCollection(). Seul le repository Mongo fourni connaît MongoDB.
Modèle générique
Une demande possède conceptuellement :
{
id,
subjectId,
documentType,
storageKey,
filename,
status,
comment,
validatorId,
createdAt,
updatedAt,
processedAt,
context
}
id identifie la demande. subjectId identifie le sujet concerné et reste opaque. documentType est défini par le consommateur. context est persisté sans interprétation.
subjectId
Le package ne connaît pas la nature du sujet : utilisateur, véhicule, entreprise, professionnel, établissement ou autre. Pour Rapidoo, une CNI peut utiliser le userId comme subjectId. Une assurance peut utiliser l'identifiant de véhicule choisi par Rapidoo. Le package ne sait jamais qu'une assurance concerne un véhicule.
documentType
Le moteur accepte toute chaîne non vide : passport, insurance, diploma, company_certificate, etc. Il ne possède aucune liste de types autorisés. Les types valides et leurs champs obligatoires appartiennent au produit.
context
context permet au consommateur de conserver les données nécessaires à ses propres conséquences métier :
context: {
country: 'CM',
userSnapshot: {
firstName: '...'
}
}
Le cœur ne lit aucune propriété de cet objet.
Statuts
DOCUMENT_VALIDATION_STATUS.PENDING
DOCUMENT_VALIDATION_STATUS.VALIDATED
DOCUMENT_VALIDATION_STATUS.REJECTED
L'objet est figé avec Object.freeze(). La V1 n'impose pas de garde de transition supplémentaire : elle extrait le comportement actuel de PMC-DI.
createDocumentValidation(options)
const validation = createDocumentValidation({
files,
repository,
hooks: {
onValidated,
onRejected,
onHookError
}
});
Le moteur valide immédiatement les contrats injectés.
Contrat files
L'objet doit fournir :
write
delete
createReadStream
L'instance recommandée provient de @aventii/files.
Contrat repository
L'objet doit fournir :
create
findById
list
count
markValidated
markRejected
delete
findLatestBySubjectAndType
Le cœur ignore comment ces opérations sont implémentées.
submit(input)
await validation.submit({
subjectId,
documentType,
storageKey,
data,
filename,
context
});
storageKey est construite par le consommateur. Le package ne génère aucune clé. data doit être un Buffer ou Uint8Array. Ordre :
files.write()
↓
repository.create()
↓
PENDING
Si la persistance échoue après l'écriture, le package tente files.delete(storageKey) puis lève PERSISTENCE_ERROR. Le cleanup de rollback est best-effort.
get(id)
const document = await validation.get(id, { scope });
scope est facultatif et opaque. Une demande absente produit NOT_FOUND.
list(options)
const documents = await validation.list({
status: 'pending',
limit: 20,
scope
});
Défauts : status = pending, limit = 20. Le repository décide comment appliquer le scope. Le repository Mongo fourni trie les plus anciennes demandes en premier, comme PMC-DI.
count(options)
const count = await validation.count({
status: 'pending',
scope
});
getValidity(input)
Capacité générique extraite de l'ancien GET /validity :
const validity = await validation.getValidity({
subjectId,
documentType,
scope
});
Le cœur recherche seulement la demande la plus récente pour subjectId + documentType. Il ne décide jamais qu'un type doit être recherché par userId, plaque ou autre identifiant métier. Sans demande :
{
"exists": false,
"valid": false
}
Avec demande :
{
"exists": true,
"valid": true,
"status": "validated",
"requestId": "...",
"createdAt": "...",
"processedAt": "...",
"comment": null
}
valid vaut true uniquement pour validated.
getFileStream(id)
const { document, stream } = await validation.getFileStream(id, { scope });
Le moteur récupère la demande puis utilise files.createReadStream(document.storageKey). Une demande sans storageKey, ou un fichier inaccessible, produit FILE_UNAVAILABLE. Le cœur ne connaît pas Express.
validate(id, options)
const document = await validation.validate(id, {
validatorId,
scope
});
Ordre V1 :
get demande
↓
cleanup fichier
↓
repository.markValidated()
↓
onValidated
↓
retour document
La suppression du fichier est best-effort. Une erreur de cleanup est journalisée mais ne bloque pas la validation. Le repository retire storageKey et filename.
reject(id, options)
const document = await validation.reject(id, {
validatorId,
comment,
scope
});
comment est obligatoire. L'ordre est le même : récupération, cleanup fichier, persistance rejected, hook onRejected.
delete(id)
await validation.delete(id, { scope });
Le moteur récupère la demande, tente de supprimer le fichier restant puis supprime l'enregistrement. Une demande inconnue produit NOT_FOUND. Il n'existe pas de hook onDeleted en V1.
Hooks
V1 :
hooks: {
onValidated,
onRejected,
onHookError
}
Les hooks servent exclusivement aux conséquences du produit : mise à jour d'un autre service, notification, métrique ou autre action externe.
onValidated
onValidated: async ({
document,
previousDocument
}) => {
// métier du produit
}
onRejected
onRejected: async ({
document,
previousDocument
}) => {
// métier du produit
}
Best-effort
Les hooks s'exécutent après la persistance. Si un hook échoue, la validation ou le rejet reste réussi. Cette sémantique reproduit le comportement actuel de PMC-DI vis-à-vis d'Access et des notifications.
onHookError
onHookError: async (error, event) => {
// logs, métriques, monitoring...
}
event.type vaut validated ou rejected. Une erreur de onHookError est elle-même journalisée sans remonter.
Repository MongoDB
const repository = createMongoDocumentValidationRepository({
getCollection
});
Avec @aventii/db :
function getCollection() {
return database
.getDb('Auth')
.collection('ValidationID');
}
getCollection() est appelée à chaque opération. Le repository ne conserve donc pas une collection liée à une éventuelle ancienne connexion remplacée par @aventii/db.
Identifiants Mongo
Le cœur manipule uniquement des chaînes opaques. Le repository Mongo convertit l'identifiant technique de la demande vers ObjectId. En sortie, _id devient id sous forme de chaîne. Les autres repositories futurs pourront utiliser UUID, bigint sérialisé ou une autre représentation sans modifier le cœur.
mapSubjectId
Par défaut :
value => value
Pour reproduire un schéma Mongo historique :
mapSubjectId: value => new ObjectId(value)
La conversion reste confinée au repository.
mapValidatorId
Même principe :
mapValidatorId: value => new ObjectId(value)
Par défaut l'identifiant est persisté tel quel.
Scope
Le scope est opaque pour le cœur. Le repository Mongo peut le traduire :
const repository = createMongoDocumentValidationRepository({
getCollection,
scopeToFilter: scope => ({
'context.country': scope.country
})
});
Puis :
await validation.list({
status: 'pending',
scope: {
country: 'CM'
}
});
Si un scope est utilisé sans scopeToFilter, le repository Mongo produit INVALID_SCOPE. Le filtre technique reste donc hors du cœur.
Futurs repositories
Le contrat permet plus tard :
MongoDB natif
Mongoose
PostgreSQL
MySQL
autre
La V1 fournit uniquement MongoDB natif, car c'est l'implémentation réellement extraite de PMC-DI. Aucun repository Mongoose ou SQL spéculatif n'est fourni.
Router Express
const router = createDocumentValidationRouter({
validation,
resolveSubmission
});
Routes relatives :
POST /
GET /
GET /count
GET /validity
GET /:id
GET /:id/file
POST /:id/validate
POST /:id/reject
DELETE /:id
Le microservice choisit le préfixe :
app.use('/di/api/validation-requests', router);
Sécurité du router
Le router n'authentifie personne. Le microservice monte lui-même ses middlewares :
app.use(
'/di/api/validation-requests',
attachUserContext,
requireAdmin,
router
);
Même principe pour rate limit, contexte interne ou règles de pays.
Soumission HTTP
Le package ne dépend pas de Multer. Le consommateur fournit obligatoirement resolveSubmission(req) :
resolveSubmission: req => ({
subjectId: req.internalContext.userId,
documentType: req.body.docType,
storageKey: buildStorageKey(req),
data: req.file.buffer,
filename: req.file.originalname,
context: buildContext(req)
})
La validation du type de document, les champs obligatoires, la clé et les snapshots restent dans le microservice.
Middleware de soumission
Un middleware peut être injecté :
createDocumentValidationRouter({
validation,
submissionMiddleware: upload.single('document'),
resolveSubmission
});
Un tableau de middlewares est également accepté. Le package ne connaît toujours pas Multer.
Validateur HTTP
Par défaut le router lit req.body.validatorId. Dans un service sécurisé :
resolveValidatorId: req => req.auth.user._id.toString()
Le package ne connaît pas req.auth.
Scope HTTP
resolveScope: req => ({
country: req.auth.user.nationality
})
Le router transmet cette valeur sans l'interpréter.
Erreurs HTTP
INVALID_INPUT, INVALID_ID et INVALID_SCOPE deviennent HTTP 400. NOT_FOUND et FILE_UNAVAILABLE deviennent HTTP 404. Les erreurs techniques inconnues sont transmises à next(err) afin que le runtime du microservice conserve la gestion du 500.
DocumentValidationError
{
name: 'DocumentValidationError',
message: '[document-validation] ...',
code: '...',
requestId: '...',
cause: Error
}
Codes V1 principaux :
INVALID_INPUT
INVALID_ID
INVALID_SCOPE
NOT_FOUND
FILE_UNAVAILABLE
PERSISTENCE_ERROR
DOCUMENT_VALIDATION_ERROR
Les erreurs de contrat à la construction utilisent TypeError.
Logger
Défaut : console. logger: null désactive les logs internes. Le moteur utilise surtout warn() pour les opérations best-effort.
Articulation avec @aventii/files
Le package ne construit aucun chemin physique. Il reçoit une storageKey et utilise l'instance injectée pour écrire, streamer et supprimer. Un futur passage LocalStorage → S3 ne doit pas modifier le workflow documentaire.
Articulation avec @aventii/db
@aventii/db garde la responsabilité du secret, de la connexion, du refresh, de getDb() et du close(). @aventii/document-validation reçoit seulement le repository. Aucune modification de @aventii/db n'est nécessaire.
Ce que le package ne fait pas
Il ne connaît pas :
- CNI, passeport, permis, carte grise, assurance ou contrôle technique ;
- véhicule, utilisateur, entreprise ou professionnel ;
- Rapidoo, GoMada ou un autre produit ;
- Access ou le service de notification ;
verifiedCni,verifiedAssuranceou tout autre champ produit ;- authentification, rôles, rate limiting ou contexte interne ;
- politique MIME ou taille maximale d'upload ;
- génération de
storageKey; - règles de champs obligatoires par type ;
- transformation d'image ou chiffrement métier ;
- métriques métier.
Fonctionnalités volontairement absentes de V1
onSubmitted
onDeleted
beforeValidate
beforeReject
transition guard
automatic retry hooks
queue / outbox
Mongoose repository
SQL repository
key generator
document type registry
Elles seront ajoutées uniquement lorsqu'un consommateur réel les exigera.
Limite connue : cleanup avant persistance
La V1 conserve volontairement l'ordre extrait de PMC-DI :
delete file
↓
persist validated/rejected
Il n'existe pas de transaction distribuée entre fichier et base. Une panne DB après suppression peut donc laisser une demande pending sans fichier. Le package documente ce comportement au lieu d'inventer une compensation non demandée.
Tests
npm test
Le package utilise node:test et node:assert/strict. Les tests couvrent notamment les contrats, la soumission, le rollback, la validité, l'ordre cleanup/persistance/hooks, les hooks best-effort, le repository Mongo, les scopes, les mappings d'ID et le router. Aucune base Mongo réelle n'est nécessaire aux tests unitaires.
Résumé
Le package extrait :
document binaire
↓
soumission
↓
pending
↓
consultation
↓
validation ou rejet
↓
destruction du fichier
↓
conséquences produit via hooks
La frontière finale est :
Le package possède le workflow documentaire. Le produit possède la signification du document et les conséquences métier de sa validation.
Dependencies
Peer Dependencies
| ID | Version |
|---|---|
| @aventii/files | ^1.0.0 |
| express | >=5 |
| mongodb | >=6 |