Repository navigation
Releases
Releases · NaufalK25/orm2erd
v2.3.0
🚀 Added
- Excalidraw support, emitting a
.excalidrawfile you open and edit in Excalidraw directly. Excalidraw has no table shape, so each entity becomes a frame named after the entity with one rectangle per field, and each relation an arrow docked to the FK and referenced column rows, using Excalidraw's own crow's-foot cardinality arrowheads. Like.drawio, the format has no auto-layout, so the emitter lays the entities out on a grid itself. (eea51b7)
💊 Fixed
- draw.io, Excalidraw, Structurizr, and Pikchr diagrams no longer contain em dashes. Field and entity descriptions, and
--names bothaliases, were separated with—. draw.io, Excalidraw, and Pikchr now use·, and Structurizr uses, alias:/, description:to match its other comma-joined column properties.--checkwill report drift on committed diagrams in these four formats that include descriptions or aliases. Regenerate and recommit to clear it. (26cfca7)
v2.2.0
🚀 Added
- erd (BurntSushi) support, rendering each entity as an
[Entity]header followed by one line per field (*marks a primary key,+a foreign key, with type/nullability/default carried in the field's{label: "..."}), and each relation as a?--1/*--1cardinality expression. Identifiers outside erd's[A-Za-z_][A-Za-z0-9_]*grammar are backtick-quoted. (0d0984a) - draw.io support, emitting a
.drawiofile you open and edit in draw.io / diagrams.net directly, instead of a text diagram language you render. Each entity becomes a real draw.io table shape (header + one row per field, with PK/FK marker cells) and each relation an edge with the matching ER cardinality markers. Since.drawiois absolute-positioned XML with no auto-layout, the emitter lays the tables out on a grid itself. (72ab38e)
💊 Fixed
- 1-1 relation labels named the wrong side.
Relation.tois always the FK-holding side, so a 1-1 swaps the two sides to put the FK onto— butfieldNamekept coming from the FK-holding side instead of following the swap, so the edge label showed the alias belonging totowhile labelling the relation fromfrom. Fixed across the Prisma, Mongoose, TypeORM, and MikroORM adapters; TypeORM and MikroORM fall back to the owning side's own property name when no inverse is declared, since that's the only alias that exists then.--checkwill report drift on any committed diagram with a 1-1 relation — regenerate and recommit to clear it. (8ac1758) - A bare
UKon a Sequelize column stranded by two overlappingbelongsToMany. TwobelongsToManydeclared through the same junction table fight over the FK attribute they share, and the second pair'sObject.assignoverwrites the first'suniquegroup — stranding one attribute as the lone member of a group whose generated name still names two columns. The attribute kept a truthyunique, so it rendered as a bareUKreading "globally unique", the opposite of what a junction table means. Such attributes are now detected by Sequelize's own${tableName}_${foreignKey}_${otherKey}_uniquenaming and excluded, leaving the composite unique intact.--checkwill report drift on affected Sequelize diagrams — regenerate and recommit. (d4764a4)
v2.1.0
🚀 Added
- Structurizr support, joining Mermaid/DBML/PlantUML/D2/Nomnoml/QuickDBD/GraphvizDOT as a registered emitter — entities render as
elements inside amodel { }block with aproperties {}block per field, relationships as->expressions, using#for comments to match the Structurizr playground's own default. (460d9e3, 26f79ca) - Pikchr support, rendering entities as boxes with a field list and relationship lines drawn between them. (1e1b6b6)
--case <mode>letter-cases rendered identifiers —preserve(default, source casing as-is),snake,screaming_snake,camel,pascal,kebab,title,lower, orupper. Only touches entity/field/enum-type names, never type labels, enum member values, or the--names bothalias. (089a106)--inflect <mode>pluralizes or singularizes entity/table identifiers only (never fields, never the--names bothalias) —preserve(default),plural, orsingular, applied before--case(e.g.--inflect plural --case kebabonPostTag→post-tags). (947ca12)-y, --yesforces non-interactive mode so every omitted flag falls back to its default instead of prompting, without needing--stdoutor a CI environment to trigger it. (49b0a77)--check --summaryprints a structural (schema-level) diff grouped by entity (e.g.users: +column "last_login_at") instead of the raw line diff, by comparing the freshly extracted model against a gitignored snapshot written alongside the diagram on the last non---checkrun. Falls back to the raw line diff, with an explanatory note, when no snapshot exists yet or the drift is formatting/ordering-only. (92d315a)- "Generated by orm2erd" watermark on every emitter's output, using each format's own comment syntax. Note: this changes the output of every existing emitter by one line — the first
--checkrun after upgrading will report drift on previously-generated files; regenerate and recommit to clear it. (82f5af4) - CLI errors now print a
(run with --help to see all flags)hint. (5d57863)
💊 Fixed
--checkwithout--outnow errors instead of silently guessing a path to verify against.--checkhas no memory of what--outwas used on the run that produced the committed file, so there was no safe default to fall back to — it's now a hard requirement, checked non-interactively before any prompting starts. (31e57ac)- Mermaid, DBML, PlantUML, and QuickDBD now quote entity/field identifiers that become syntactically unsafe after
--case kebab(bare hyphen) or--case title/other space-containing modes, instead of emitting broken output — the exact trigger character(s) and quote style differ per format, verified empirically against each one's own grammar. (a63af11)
v2.0.0
⚠️ Breaking
- Default entity/field names switched from ORM model names to physical table/column names. New
--names <table|model|both>flag controls this —table(physical names, e.g. Sequelize's pluralizedUserstable or Prisma's@@map-ed name) is now the default, since an ERD describes a database and should match what you'd see inpsql;modelrestores the previous default (the ORM's own model/field names), andbothrenders the physical name with the ORM name as an alias where the output format supports one. A companion--relation-label <both|alias|column>flag (defaultboth) controls whether relation edges show the association alias, the FK column, or both when they disambiguate two relations between the same entity pair. Existing--check/CI pipelines that don't pass--names/--relation-labelexplicitly will see diagrams regenerate with different content under the new default — pass--names modelto keep prior output unchanged. (e290816) --out <name>resolves inside an existing same-named directory instead of next to it. Previously a bare, extension-less--out erdalways resolved to a sibling file,erd.mmd, regardless of what else existed on disk. Now, iferdalready exists as a directory, the same flag resolves toerd/erd.mmdinstead — silently different output for anyone who already had a directory at that path (e.g. a folder collecting several exported diagrams), breaking--check/CI configs and any script reading the old sibling-file location. Only bare names colliding with an existing directory are affected; a trailing slash (--out erd/) was always directory-aware and a name with no on-disk collision is unchanged. (c73a3f7)
🚀 Added
- MikroORM support, targeting both v6 (
@mikro-orm/core@^6, classic@Entity()/@Property()decorators) and v7 (@mikro-orm/core@^7) via the same adapter — entities, fields, relations, and composite keys are read from MikroORM's owngetMetadata().getAll(). The one real behavioral difference between the two majors —getAll()returning a plainDictionaryin v6 vs. a realMapin v7 — is branched on viainstanceof Mapso both are normalized the same way; v6's mandatoryconnect: false(an unreachable DB otherwise kills discovery entirely) is a harmless no-op pass-through under v7, which dropped auto-connect. See docs/adapters.md for the full detection/entry/parsing details. (c63f7d4, 7c3dc31) --stdoutprints the generated diagram to stdout instead of writing a file (requires exactly one--format); status/log lines route to stderr so the diagram itself can be piped cleanly, e.g.orm2erd ... --stdout > erd.mmd. (9d4294f)--copycopies the generated diagram straight to the clipboard instead of writing a file (requires exactly one--format), viaclipboardy(pure-JS, cross-platform, including WSL). (8c6dea1)--format allnow works non-interactively too (previously only reachable via the interactive multi-select), expanding to every registered emitter. (c6d2953)- Detection confidence for ORMs with multiple entry-point candidates is now computed dynamically (
1 / candidateCount, rounded) instead of a hardcoded 0.5 for any 2+ candidates — three candidates now correctly reads as 33% instead of the same 50% shown for two. (b405325)
💊 Fixed
- DBML: one-to-many relations emitted the
>ref operator, which DBML tooling reads as the left column allowing "many" — backwards, since the IR'sfromside is always the one/referenced end. Now emits<instead (rel.fromstays on the left;<reads correctly for one-to-many, matching the invariant thatrel.tois always the FK-holding side). Optional relations (isFromOptional) also now emit DBML's?prefix, e.g.users.id ?< posts.editor_id. (7857da1)
v1.9.0
🚀 Added
- Optional-relation support. The IR's
RelationgainedisFromOptional, marking whether thefrom(referenced/parent) end is optional — i.e. the FK column on theto(child) side is nullable, so a child row can exist without a parent. All five adapters now resolve it: Prisma fromfield.isRequired, Sequelize/Drizzle from the FK column's own nullability (both quirkily never setallowNull/notNullon primary keys, so that's special-cased to always non-nullable), TypeORM from the owning join column'sisNullable, and Mongoose frompath.isRequiredon whichever side holds the ref. Getting adapters to report this consistently also meant fixing the relationfrom/toassignment itself in a few spots (Prisma 1-1, TypeORM one-to-one, Mongoose paired/standalone refs) so the FK-holding side always lands onto, matching the invariant every other relation shape already followed. Mermaid renders it directly: thetoend is alwayso(nothing can force a parent to have a child), while thefromend downgrades from||to|owhenisFromOptionalis set. (ac86c73) - Physical table/column names in the IR.
Entity.tableNameandField.columnNameare now populated whenever the physical name differs from the logical one — Prisma's@@map/@map, Sequelize'stableName/fieldoptions, TypeORM'sentityMetadata.tableName/column.databaseName, and Mongoose'smodel.collection.name. Drizzle is exempt: itsname/getColumnName()already resolve the physical SQL name directly (it has no separate ORM-level model name to diverge from), sotableName/columnNamewould always just equalnamethere. Not yet rendered by any emitter — this lays the groundwork for surfacing physical names in generated diagrams. (5015f1c)
💊 Fixed
- Two relations sharing the same alias but different FK columns (e.g. two FKs from the same entity pair) rendered identical, ambiguous edge labels. A new
relationLabel()helper (emitters/label.ts) appends the FK column in parentheses when it differs from the field name — e.g.author (authorId)— used by Mermaid, D2, and PlantUML. (6f21524)
v1.8.0
🚀 Added
- Per-phase progress during generation. The CLI's spinner now updates its label through
Resolving entry…→Parsing schema…→Generating diagram(s)…→Writing output…instead of sitting on a single genericGenerating...for the whole run — useful feedback on larger schemas where parsing alone can take a few seconds. Non-interactive mode prints each phase as its own log line instead. Output-suppression duringextract()is bypassed for the spinner's own writes specifically, and each phase yields briefly before starting so its label has a chance to paint before a long synchronous phase (e.g. importing the target's models) starves the spinner's redraw interval. (1cceee2)
💊 Fixed
- Sequelize: an FK column declared only via
HasMany/HasOne/BelongsToMany(i.e. every association type other thanBelongsTo) was missed byisForeignKey, since 1.7.0's fix matched FK-ness againstBelongsToassociations specifically. Reading the attribute's own resolvedreferences(set byaddForeignKeyConstraintsregardless of which side declared the association) is accurate for all of them instead of reconstructing FK-ness from association direction. (a399138) - Sequelize/Drizzle/TypeORM: array columns lost their element type. Sequelize's
DataTypes.ARRAYwraps its element type rather than exposing it directly, so an array column fell through tounknownwith no list marker; Drizzle's.array()columns (e.g. pg-core'sPgArray) wrap theirs on.baseColumnthe same way; TypeORM's@Column("text", { array: true })setsColumnMetadata.isArray, which wasn't read at all. All three now unwrap to the element for canonical type, native type, andenumValues, and setField.isListso emitters render the[]suffix. Also fixed, in the same pass: Sequelize'sDataTypes.JSONrendered asJSONTYPE(its internal class name) instead ofJSON(its public type key, now preferred when present), and canonical-type coverage was extended to several previously-unmapped Sequelize types (UUID,MEDIUMINT,DOUBLE PRECISION,TIME,VIRTUAL,RANGE,GEOMETRY,GEOGRAPHY,HSTORE,INET,CIDR,MACADDR). (dc3c817) - Sequelize: a composite unique declared via the
unique: 'groupName'shorthand on individual attributes was silently dropped, since that grouping only ever surfaces onmodel.uniqueKeys— never onmodel.options.indexes, which the adapter read exclusively. Both sources are now read and merged (deduped by sorted field list, in case the same group is redundantly declared through both). Mermaid, D2, GraphViz DOT, and PlantUML now also mark a fieldUKand annotate its composite-unique group members ("unique with: ...") when it's unique only by virtue of multi-column membership, not its ownisUniqueflag. (2a73940)
v1.7.0
🚀 Added
- Drizzle support. Detection requires
drizzle-ormin dependencies and checks fordrizzle.config.ts, then.js, then.jsonat the project root — the same priority orderdrizzle-kit's own CLI falls back through. Extraction takes the config file itself as the entry point: itsdialectfield selects whichdrizzle-ormdialect-core package (pg-core/mysql-core/sqlite-core/singlestore-core) to introspect tables with, and itsschemaglob(s) point at the actual table files, all imported viatsx'stsImport()and read throughdrizzle-orm's ownis()/getTableConfig()— the target project's own installeddrizzle-orm, not orm2erd's. Supports composite primary keys, multi-column unique constraints and indexes (including functional/expression indexes resolved via the dialect'ssqlToQuery), the project'scasing: "camelCase" | "snake_case"strategy for columns with no explicit name,onDelete/onUpdaterelation actions, and rawsqldefaults. Cardinality falls back to FK-column uniqueness the same way the Sequelize adapter's undeclaredBelongsTodoes, since Drizzle has no separateOneToOne/ManyToOnedeclaration to trust instead. Drizzle has no comment/description option on a column or table, sodescriptionis never populated. See the new "Drizzle" section indocs/adapters.md. (b926c03) - Programmatic API.
orm2erdnow exportsdetectORMs,getAdapter,getEmitter, and theERDModel/Entity/Field/Relation/IndexIR types from its package root (src/index.ts), mirroring the CLI's own detect → extract → emit pipeline for use as a library, not just a CLI. (871a9e7) - Grid layout for the interactive format picker. The
Output format(s)prompt now lays options out in an auto-sized grid — column count computed from terminal width and label length, capped at 3 — instead of one per line, with arrow keys navigating by row/column. Ragged last rows (e.g. 7 options in 3 columns) are handled explicitly so up/down/left/right always land on a real cell instead of wrapping into the wrong one. (0d8be5a)
💊 Fixed
- Sequelize: a plain column that happened to share its name with another model's foreign key was incorrectly marked as an FK itself, because the FK-column set was built from every association's
foreignKeyregardless of association type. OnlyBelongsToassociations contribute now, sinceHasMany/HasOnename a column on the target model, not the one declaring the association. (fba2390) - Sequelize: a 1-1 relation backed only by a lone
BelongsTo(no reciprocalHasOneregistered) always rendered1-1even when the FK column wasn't actually unique — indistinguishable from an undeclaredHasMany's "many" side. It's now trusted as1-1only when an explicitHasOnepairing exists or the FK column is itself unique; otherwise it renders1-n, and the parent/child direction is normalized consistently with theHasManycase above. (fba2390)
v1.6.0
🚀 Added
--checkflag: regenerates the ERD in memory and compares it against what's already on disk instead of writing — up to date exits0; drifted prints a diff and exits1; missing exits1too. Never touches the filesystem, so it's safe in a pre-commit hook or CI. The diff highlights only the changed words within an edited line (via an LCS-based word diff), not the whole line, alongside plain+/-for added/removed lines. Forces non-interactive mode so it never blocks on a prompt. See the new "Keeping the ERD in sync (CI)" README section for a drop-in GitHub Actions workflow. (bb2249e, b43c3c5)- Composite primary keys and multi-column unique constraints.
Entity.primaryKey/.uniquescarry the ordered member columns of a composite key/multi-column unique across all four adapters (Prisma's@@id/@@unique, Sequelize'sprimaryKeyAttributes/options.indexes, Mongoose's compoundschema.indexes(), TypeORM'sprimaryColumns/uniques); single-column keys still use the existing per-fieldisPrimaryKey/isUnique. DBML renders these as a nativeindexes { (a, b) [pk] }/[unique]block; other emitters mark the member fields individually. (8c8287c) - Plain (non-unique) indexes.
Entity.indexescarries single- or multi-column lookup indexes (Prisma@@index, Sequelizeoptions.indexes, Mongooseschema.indexes(), TypeORM@Index()), rendered as DBMLindexes { ... }entries; other emitters have no equivalent construct and skip them. (02cc05a) - Descriptions and comments. Doc comments (Prisma's
///, Sequelize'scommentoption, TypeORM's@Entity({ comment })/@Column({ comment })) are read ontoEntity.description/Field.descriptionand rendered as a DBMLNote/inlinenote:, a Mermaid comment line/trailing annotation, or a PlantUML note. Mongoose has no comment concept on a schema, so its adapter never populates these. (1db3ca1) onDelete/onUpdaterelation actions. Referential actions declared on a foreign key (Prisma's@relation(onDelete: ...), Sequelize's FK attribute options — including the defaults Sequelize itself applies when nothing is declared, and TypeORM's@ManyToOne/@OneToOneoptions) are carried onRelation.onDelete/.onUpdateand rendered as DBML's[delete: cascade, update: restrict]. Mongoose has no FK-constraint concept, so its adapter never populates these. (f466993)- GraphViz DOT output (
--format graphvizdot), emitting an HTML-label table per entity withPK/FK/UKmarkers and directed edges with crow's-foot-style arrowheads for1-1/1-n/n-ncardinality. (bcbe496) - Friendlier errors for common Sequelize/Mongoose/TypeORM extraction failures: a missing target-project dependency, a missing
reflect-metadataimport, an eager database connection attempt at import time, or a CommonJS/ESM mismatch now get an actionable hint appended below the raw error, instead of surfacing only the bare error message. (c68bbbd)
💊 Fixed
- A Sequelize
BelongsToManywhosethroughjunction is itself an emitted entity produced a redundant derivedn-nrelation on top of the two1-nedges already implied by the junction's own associations. The derived edge is now suppressed whenever the junction is actually rendered as its own table (an implicit, unnamed join table that isn't emitted as an entity still keeps itsn-nedge, since nothing else conveys the relationship for it). (c90e697) - DBML
Note/index-name values were wrapped in single quotes with embedded single quotes escaped to double quotes, which read oddly for descriptions that were themselves already double-quoted prose. Both are now consistently double-quoted, with embedded double quotes escaped to single quotes instead. (cff5807)
v1.5.0
🚀 Added
- TypeORM support. Detection recognizes legacy 0.2.x
ormconfig.*(JSON) alongside the 0.3+ convention of adata-source.ts/.jsfile exporting aDataSource, falling back to a content scan when neither exists — the same tiered approach as Sequelize/Mongoose detection. Extraction builds metadata via TypeORM's own internalConnectionMetadataBuilder(the same class TypeORM itself uses insideDataSource#initialize(), before ever opening a real connection), so it stays DB-connection-free..tsentity/DataSource files are compiled with the target project's own installedtypescript+tsconfig.jsonfirst, since TypeORM's decorators rely onemitDecoratorMetadata/the legacyexperimentalDecoratorscalling convention, neither of whichtsx's esbuild-based transform can emit correctly. Supports decorator entities,EntitySchema(plain-object) entities, and legacy 0.2.x connections built without ever calling.connect(). (6db88ff) - QuickDBD output (
--format quickdbd), emitting dbdiagram.io's QuickDBD syntax with inlinePK/FK/UNIQUE/NULLfield constraints — QuickDBD has no separate relationship section, so the FK marker sits directly on whichever field physically holds the key — plus enum/default values as trailing#comments. (69528f2) - Colorized CLI output (help text, prompts, success/error messages) via
picocolors, plus an icon on each interactive step (detection, entry point, format, output path, type mode, generating, result). Both color and icons auto-disable in environments without color/Unicode support (CI,NO_COLOR, non-UTF8 terminals), using the same detection heuristic@clack/promptsalready relies on for its own prompt symbols — falling back to plain ASCII (o/x) for status icons and to nothing for purely decorative ones. (5e19229)
💊 Fixed
- The Mermaid emitter's
%% Entities/%% Relationshipssection comments weren't indented to match the rest of the diagram body. (bd2d1eb)
v1.4.0
🚀 Added
- D2 output (
--format d2), emittingsql_table-shaped nodes withpk/fk/uniquefield constraints,NOT NULL/DEFAULT ...inline comments, and crow's-foot cardinality viacf-one/cf-manyarrowhead shapes on<->connections (D2 has no inline symbol for it like Mermaid/DBML). Identifiers are always quoted to sidestep D2's reserved top-level keywords (shape,style,layers, …). (81d9fc0) - nomnoml output (
--format nomnoml), emitting<table>-classifier nodes withPK/FK/unique/NNfield tags and1 -- 1/1 -- */* -- *multiplicity relations. (ec375fd) - Every emitter now separates entities and relationships with an
// Entities/// Relationshipsheading (or the format's own comment syntax) and a blank line between entity blocks, instead of one unbroken stream of lines. (c9c645c)
💊 Fixed
- Target codebases whose entry file logs straight to
process.stdout/process.stderr(e.g. pino, morgan, winston's default console transport) instead of going throughconsole.*leaked that output past--verbose's suppression, since onlyconsole.*was being patched. Both streams are now patched too. (f523e5c) - The CLI could hang after a successful run instead of exiting — importing the target codebase to introspect it (a DB connection, timers, etc.) can leave open handles that hold Node's event loop open forever. The process now exits explicitly once output is written. (9829cb6)
Sequelize.literal(...)default values (e.g.nextval('posts_id_seq')for a Postgres sequence default) were resolved to the wrapper's constructor name instead of the actual SQL expression — the fallback for sentinelDataTypesinstances (UUIDV4,NOW, …) matched it too, since both are property-less class instances.Literal's wrappedvalis now read directly instead. (6f20968)- A default value containing a double quote (e.g. a JSON default like
{"a":""}) broke the emitted DBML/Mermaid output by prematurely closing the surrounding quoted attribute. Embedded double quotes are now escaped to single quotes before emitting, consistent with how string defaults are already handled elsewhere. (bb452ee)