# @mostajs/orm — fiche LLM
> ORM multi-dialecte inspiré d'Hibernate — une seule API, 13 bases de données, zéro lock-in.

- Version: 2.2.4 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
- Chemin: mostajs/mosta-orm · Statut audit: complet (dist/) · Anomalies Lot 3 corrigées (#1-#9 cf. docs/ANOMALIES-LOT3-2026-05-25.md ; scanner intégré scan-silent-bugs.mjs)

## RÔLE
Couche d'accès aux données unifiée pour Node.js/TypeScript. Le développeur décrit
ses entités sous forme d'objets `EntitySchema` (fields, relations, indexes) et obtient
une API CRUD/query identique quel que soit le SGBD cible : MongoDB, SQLite, PostgreSQL,
MySQL, MariaDB, Oracle, MSSQL, CockroachDB, DB2, SAP HANA, HSQLDB, Spanner, Sybase.
Concepts empruntés à Hibernate/JPA (CascadeType, FetchType, schemaStrategy = hbm2ddl.auto,
ConnectionConfig = persistence.xml). C'est le module fondation de l'écosystème mostajs.

## INSTALLATION
npm i @mostajs/orm
(driver natif requis selon dialecte : better-sqlite3, pg, mysql2, mariadb, oracledb,
mssql, ibm_db, @sap/hana-client, @google-cloud/spanner, mongoose — peer/optional deps)

## EXPORTS
- Registre schémas : registerSchema, registerSchemas, getSchema, getSchemaByCollection,
  getAllSchemas, getEntityNames, hasSchema, validateSchemas, clearRegistry
- Connexion/factory : getDialect, getConfigFromEnv, getCurrentDialectType, disconnectDialect,
  testConnection, createConnection, createIsolatedDialect, createDatabase, dropDatabase,
  registerNamedConnection, getNamedConnection, removeNamedConnection, listNamedConnections,
  clearNamedConnections
- Dialectes (métadonnées) : DIALECT_CONFIGS, getSupportedDialects, getDialectConfig
- Repository : BaseRepository, EntityService
- Introspection : OrmIntrospectionError, resolveLookup, findMatchingUniqueIndex, extractRelId
- Normalisation : normalizeDoc, normalizeDocs
- Migration : diffSchemas, generateMigrationSQL
- Erreurs : MostaORMError, EntityNotFoundError, ConnectionError, ValidationError, DialectNotFoundError
- Config (ré-export @mostajs/config) : getEnv, getEnvBool, getEnvNumber, getCurrentProfile
- Types : FieldType ('string'|'text'|'number'|'boolean'|'date'|'json'|'array'),
  FieldDef, EmbeddedSchemaDef, RelationType ('one-to-one'|'many-to-one'|'one-to-many'|'many-to-many'),
  RelationDef, CascadeType ('persist'|'merge'|'remove'|'all'),
  FetchType ('lazy'|'eager'), OnDeleteAction ('cascade'|'set-null'|'restrict'|'no-action'),
  IndexType, IndexDef, EntitySchema, FilterOperator, FilterValue, FilterQuery,
  SortDirection, QueryOptions, PaginatedResult, AggregateStage (et variantes),
  DialectType, SchemaStrategy ('validate'|'update'|'create'|'create-drop'|'none'),
  ConnectionConfig, IDialect, IRepository, IPlugin, HookContext, NormalizedDoc,
  TxHandle ({ id: string; startedAt: number; depth: 1=outermost / 2+=nested SAVEPOINT } — expérimentale),
  DialectConfig, ResolvedLookup, UniqueIndexMatch, DiffOperation (14 variantes — cf. TYPES CLÉS),
  OrmRequest, OrmResponse, OrmOperation

## EXPORTS PAR SOUS-CHEMIN
- `@mostajs/orm/register` : `register(registry: { register(r: ModuleRegistration): void }): void`
  — déclare l'ORM dans le graphe de dépendances @mostajs/socle (`ModuleRegistration` provient de
  `@mostajs/socle`). Le module fondation n'expose ni schémas ni repos propres ; cette fonction
  rend simplement l'ORM visible aux modules en aval qui veulent dépendre de lui par déclaration.
- `@mostajs/orm/bridge` : pont JDBC vers SGBD non-natifs Node via process Java.
  JdbcNormalizer, parseUri, JDBC_REGISTRY, hasJdbcDriver, getJdbcDriverInfo, BridgeManager,
  saveJarFile, deleteJarFile, listJarFiles, detectDialectFromJar, getJdbcDialectStatus ;
  types BridgeInstance, JarUploadResult, JdbcDriverInfo, JdbcBridgeConfig.
- `@mostajs/orm/validator` : linter conceptuel de schémas (24 règles : R001-R018
  + R003B + R004B + R013B + R019/R020/R021 ajoutées en 2.1.0 — voir bloc PIÈGES).
  validateSchemas, DEFAULT_RULES, applyFixes, rollbackFixes, formatText/formatJson/formatMarkdown,
  DEFAULT_SOFT_DELETE_PATTERNS, DEFAULT_AUDIT_BY_FIELDS, DEFAULT_THRESHOLDS ; types Severity,
  Finding, Report, ValidateOptions, Rule, RuleContext, FixOptions, FixResult, RollbackResult.
  Bin : `mostajs-orm-validator`.
  - `applyFixes(report: Report, opts: FixOptions): Promise<FixResult[]>`
    `FixOptions = { sourceRoot: string; dryRun?: boolean = true; rules?: string[]; backup?: boolean = true }`
    `FixResult = { ruleId, schema, field?, file, applied, reason?, diff?, description }`
  - `rollbackFixes(sourceRoot: string): RollbackResult[]` — restaure tous les `<file>.bak`
    présents sous `sourceRoot`, supprime les `.bak` après restauration ; idempotent.
  Les règles R019-R021 nécessitent `sourceRoot` dans les options pour faire de la
  détection AST cross-file via ts-morph sur le code consumer. Auto-fix R019/R021
  livré 2.1.0 (insert import extractRelId + wrap, idempotent, .bak backup).
  Règles ré-exportées individuellement (pour composition opt-in via Rule[] custom) :
  R001_EMPTY_RELATIONS, R002_FK_NAMING, R003_SOFT_DELETE,
  R003B_UNIQUE_WITH_SOFTDELETE_CONFLICT, R004_DUPLICATE_ENTITY,
  R004B_LEGACY_ENTITY, R005_ANY_TYPED_REPO, R006_JSON_AS_RELATION,
  R007_REDUNDANT_DERIVED_FIELD, R008_BEST_EFFORT_RESOLVER, R009_MISSING_LOOKUP_INDEX,
  R010_MISSING_AUDIT_TABLE, R011_LEGACY_DEAD_CODE, R012_DUPLICATE_IMPLEMENTATION,
  R013_MISSING_CASCADE, R013B_EAGER_WITHOUT_CASCADE,
  R014_REPO_FACTORY_BOILERPLATE, R015_FLAT_LIB_STRUCTURE,
  R016_AUDIT_EMAIL_AS_STRING, R017_UNBOUNDED_BLOB, R018_EXTERNAL_SCHEMA_OVERSCOPED,
  R019_FINDBYID_OBJECT_INPUT, R020_NATURAL_KEY_LOOKUP_OPPORTUNITY,
  R021_DIRECT_RELATION_COMPARISON.

## API — SIGNATURES
- getDialect(config?): Promise<IDialect> — singleton, reconnexion idempotente
- getConfigFromEnv(): ConnectionConfig — lit DB_DIALECT + SGBD_URI (cascade MOSTA_ENV)
- createConnection(config, schemas?): Promise<IDialect> — connecte + enregistre schémas
- createIsolatedDialect(config, schemas?): Promise<IDialect> — connexion neuve hors singleton
- testConnection(config): Promise<{ ok: boolean; error?: string }>
- createDatabase / dropDatabase(dialect, uri, dbName): Promise<{ ok; detail?; error? }>
- registerSchema(schema) / registerSchemas(schemas) : void
- getSchema(name): EntitySchema (throw si absent) ; getAllSchemas(): EntitySchema[]
- validateSchemas(): { valid: boolean; errors: string[] } — vérifie cibles de relations
- diffSchemas(oldSchemas, newSchemas): DiffOperation[]
- generateMigrationSQL(ops): string[]
- class BaseRepository<T extends { id: string }> implements IRepository<T>
  ctor(schema: EntitySchema, dialect: IDialect)
  findAll(filter?, options?) · findOne(filter, options?) · findById(idOrEntity, options?)
  findByIdWithRelations(id, relations?, options?) · create(data) · update(id, data)
  updateMany(filter, data) · delete(id) · deleteMany(filter)
  count(filter?, options?) · search(query, options?)
  distinct(field, filter?, options?) · aggregate(stages, options?) · upsert(filter, data)
  increment(id, field, amount) · addToSet(id, field, value) · pull(id, field, value)
  findWithRelations(filter, relations, options?)
  Toutes les méthodes read acceptent `options?: QueryOptions` ; `includeDeleted: true`
  bypasse le filtre soft-delete (2.2.0).
- class EntityService extends EventEmitter
  ctor(dialect) ; getRepo(name): BaseRepository ; getEntityNames() ; getEntitySchema(name)
  hasEntity(name) ; findAll/findOne/findById/create/update/delete/count(...) ;
  execute(req: OrmRequest): Promise<OrmResponse>
- IDialect : connect/disconnect/testConnection/initSchema + CRUD + aggregate + findWithRelations
  + upsert/increment/addToSet/pull/search ; signatures notables (2.2.0) :
  - `count(schema, filter, options?: QueryOptions)`
  - `distinct(schema, field, filter, options?: QueryOptions)`
  - `aggregate(schema, stages, options?: QueryOptions)`
  Optionnels :
  - executeQuery / executeRun (SQL brut, opt-in dialect)
  - $transaction<T>(cb, opts?: { isolation?: 'READ UNCOMMITTED'|'READ COMMITTED'|'REPEATABLE READ'|'SERIALIZABLE' }): Promise<T>
  - beginTx(opts?: { isolation? }): Promise<TxHandle> · commitTx(tx): Promise<void> · rollbackTx(tx): Promise<void>
    (API tx manuelle, expérimentale ; `poolSize: 1` requis sur dialectes SQL poolés ; MongoDB : utiliser $transaction(cb))
  - dropTable / dropAllTables / dropSchema / truncateTable / truncateAll.

  Mapping isolation par dialect (2.2.0) :
  - SQLite : ANSI → DEFERRED/IMMEDIATE/EXCLUSIVE
  - MySQL/MariaDB : `SET SESSION TRANSACTION ISOLATION LEVEL X; START TRANSACTION`
  - Oracle : 2 niveaux uniquement (READ COMMITTED, SERIALIZABLE) — mapping auto pour les 4 ANSI
  - HANA : READ UNCOMMITTED non supporté → mappé à READ COMMITTED
  - DB2 : ANSI → UR/CS/RS/RR (vocabulaire DB2 natif)
  - Postgres/CockroachDB/MSSQL : ANSI nativement supportés

## TYPES CLÉS
- EntitySchema : { name, collection, fields: Record<string,FieldDef>,
  relations: Record<string,RelationDef>, indexes: IndexDef[], timestamps: boolean,
  discriminator?, discriminatorValue?, softDelete? }
- FieldDef : { type: FieldType, required?, unique?, sparse?, default?, enum?, lowercase?,
  trim?, arrayOf? }  · FieldType = 'string'|'text'|'number'|'boolean'|'date'|'json'|'array'
- RelationDef : { target, type: RelationType, required?, select?, nullable?, through?,
  cascade?: CascadeType[], orphanRemoval?, fetch?: FetchType, mappedBy?, joinColumn?,
  inverseJoinColumn?, onDelete? }
- RelationType = 'one-to-one'|'many-to-one'|'one-to-many'|'many-to-many'
- ConnectionConfig : { dialect, uri, showSql?, formatSql?, highlightSql?, schemaStrategy?,
  poolSize?, cacheEnabled?, cacheTtlSeconds?, batchSize?, options? }
- SchemaStrategy = 'validate'|'update'|'create'|'create-drop'|'none' (= hbm2ddl.auto)
- FilterQuery : objet MongoDB-like ($eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$regex/$exists, $or/$and)
- QueryOptions : { sort?, skip?, limit?, select?, exclude?, includeDeleted? }
  - `includeDeleted?: boolean` (2.2.0) — bypasse le filtre soft-delete automatique
    sur read methods. Sans effet si `softDelete: true` n'est pas activé sur le schéma.
- IPlugin : hooks onSchemaInit/preSave/postSave/preUpdate/postUpdate/preDelete/onQuery/onResult
- TxHandle (lecture seule) : `{ id: string; startedAt: number; depth: 1|2|... }`
  — `depth === 1` = transaction réelle (BEGIN/COMMIT), `depth >= 2` = nested SAVEPOINT.
- DiffOperation (union discriminée — 14 variantes, type = discriminant) :
  - `{ type: 'addEntity'; schema: EntitySchema }`
  - `{ type: 'removeEntity'; entity: string; collection: string }`
  - `{ type: 'addField'; entity; field; def: FieldDef }`
  - `{ type: 'removeField'; entity; field }`
  - `{ type: 'alterField'; entity; field; from: FieldDef; to: FieldDef }`
  - `{ type: 'addIndex'; entity; index: IndexDef }` · `{ type: 'removeIndex'; entity; index }`
  - `{ type: 'renameCollection'; entity; from; to }`
  - `{ type: 'addTimestamps'; entity }` · `{ type: 'removeTimestamps'; entity }`
  - `{ type: 'addSoftDelete'; entity }` · `{ type: 'removeSoftDelete'; entity }`
  - `{ type: 'addDiscriminator'; entity; field; value }` · `{ type: 'removeDiscriminator'; entity; field }`

## ERREURS TYPÉES — QUAND LEVÉES
- `MostaORMError(message)` — racine de toutes les erreurs ORM (catch générique).
- `EntityNotFoundError(entityName, id?)` — levée par `update(id, …)` / `delete(id)` quand `id`
  n'existe pas. Pas levée par `findById` / `findOne` (qui retournent `null`).
- `ConnectionError(dialect, cause?)` — levée par `createConnection` / `testConnection`
  quand l'URI est invalide, le driver natif manque, ou le SGBD refuse la session.
- `ValidationError(entityName, details)` — levée à `create` / `update` quand un field `required`
  est absent, qu'un `enum` est violé, ou qu'un `unique` est dupliqué (selon dialect).
- `DialectNotFoundError(dialect)` — levée par `getDialect({ dialect: 'inconnu' })` si la valeur
  ne correspond à aucun `DialectType` supporté (cf. `getSupportedDialects()`).
- `OrmIntrospectionError` (introspection, pas une `MostaORMError`) — levée par `findById({…})`
  quand l'objet ne matche ni `id`, ni un index unique ; expose `schemaName`, `input`,
  `availableFields`, candidates de matching pour message actionnable.

## PATTERN
```ts
import { createConnection, BaseRepository, type EntitySchema } from '@mostajs/orm';

const UserSchema: EntitySchema = {
  name: 'User', collection: 'users',
  fields: { email: { type: 'string', required: true, unique: true },
            name:  { type: 'string' } },
  relations: {}, indexes: [], timestamps: true,
};

const dialect = await createConnection(
  { dialect: 'sqlite', uri: './app.db', schemaStrategy: 'update' },
  [UserSchema],
);
const users = new BaseRepository<{ id: string; email: string; name?: string }>(UserSchema, dialect);
await users.create({ email: 'a@b.c', name: 'Alice' });
const found = await users.findOne({ email: 'a@b.c' });
```

## DÉPEND DE
- Runtime : @mostajs/config (cascade env). peerDeps : @mostajs/socle (graphe modules).
- Drivers SGBD en peer/optional deps (installer seulement ceux utilisés).
- Pas d'autre dépendance @mostajs/* obligatoire — c'est le module fondation.

## PIÈGES
- BREAKING 2.0 : toutes les relations sont `lazy` par défaut (avant : many-to-one et
  one-to-one étaient eager). `findById()` retourne désormais la string id pour ces relations,
  pas l'objet populé. Utiliser `findByIdWithRelations` / `findWithRelations` pour populer,
  ou `extractRelId(value)` pour des comparaisons sûres en lazy comme en eager.
- `getDialect()` est un singleton ; pour plusieurs connexions simultanées utiliser
  `createIsolatedDialect()` (hors singleton, schémas non enregistrés globalement).
- `schemaStrategy: 'create'` drop puis recrée ; `'create-drop'` drop à l'arrêt. En prod : `'validate'`.
- `softDelete: true` sur un `EntitySchema` :
  - **injecte automatiquement** le champ `deletedAt: Date | null`. Pas besoin de le déclarer.
  - **filtre automatiquement** `findAll` / `findOne` / `findById` / `count` / `search` /
    `aggregate` sur `deletedAt IS NULL` (SQL) ou `deletedAt: null` (Mongo).
  - `delete(id)` devient un soft-delete (UPDATE `deletedAt = now`), pas un DELETE physique.
  - **Pas d'option `includeDeleted`** côté repo en 2.1.0. Pour récupérer une ligne soft-deletée,
    filtrer explicitement : `findOne({ deletedAt: { $ne: null } })` ou `findAll({ deletedAt: { $exists: true } })`.
  - Compatible `sparse: true` sur unique indexes → réinsertion possible après soft-delete (R003B).
- API tx manuelle (beginTx/commitTx/rollbackTx) : expérimentale, type `TxHandle`. Sur dialectes
  SQL poolés, forcer `poolSize: 1` pour la cohérence stricte ; MongoDB : utiliser `$transaction(cb)`.
- Ne jamais mettre `cascade: ['remove'|'all']` sur une relation many-to-many (supprimerait l'entité cible).
- `dropDatabase` est IRRÉVERSIBLE. `findById(objet)` lève `OrmIntrospectionError` si l'objet
  ne correspond ni à `id` ni à un index unique.
- Multi-dialecte FK : sur SQL, les contraintes `FOREIGN KEY ... ON DELETE ...` sont générées
  depuis `relations[name].onDelete` à la création de schema ; sur MongoDB, il n'y a pas de
  FK natives — `findWithRelations` utilise `.populate()` (mongoose ref). L'API JS reste
  identique ; le dialect adapte. Conséquence : `onDelete: 'cascade'` ne s'applique côté
  serveur que sur SQL — sur MongoDB il faut implémenter manuellement la cascade (cf. R013).
- Validator 2.1.0 — nouvelles règles :
  Cross-file consumer-code (nécessitent `sourceRoot`) :
  - **R019-FINDBYID-OBJECT-INPUT** *(warning, fixable)* — flag `repo.findById(entity.relation)`
    où relation est déclarée comme relation. Auto-fix : insert import extractRelId + wrap.
  - **R020-NATURAL-KEY-LOOKUP-OPPORTUNITY** *(info, non-fixable par design)* — flag
    `repo.findOne({field:x})` quand `field` (ou composite) est un unique index ;
    `findOne` reste valide.
  - **R021-DIRECT-RELATION-COMPARISON** *(warning, fixable)* — flag
    `entity.relation === value` : sous `fetch:'eager'`, toujours faux. Auto-fix wrap.
  Schema-only (pas besoin de `sourceRoot`) :
  - **R003B-UNIQUE-WITH-SOFTDELETE-CONFLICT** *(warning, fixable)* — flag les index
    unique non-sparse sur schémas avec soft-delete (natif ou pattern manuel) — bloque
    la réinsertion après soft-delete. Auto-fix : ajoute `sparse: true`.
  - **R013B-EAGER-WITHOUT-CASCADE** *(warning, fixable)* — flag les relations
    `fetch: 'eager'` sans `onDelete` — orphelins populés silencieusement. Auto-fix
    insère un `onDelete` cohérent ('cascade' si required ou one-to-many, sinon 'set-null').

## RÉFÉRENCES
- README.md · CHANGELOG.md
- docs/ORMConceptValidator.md, docs/TECHNIQUE-INTROSPECTION-FINDONEBYID.md,
  docs/MONITORING-orm.md, docs/IDEE-INTRASPECTION-SCHEMAT.md
