@aventii/document-validation (1.0.0)

Published 2026-08-24 11:06:02 +00:00 by Valentin

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, verifiedAssurance ou 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
Details
npm
2026-08-24 11:06:02 +00:00
1
UNLICENSED
latest
12 KiB
Assets (1)
Versions (1) View all
1.0.0 2026-08-24