ormbridge 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +408 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -0
- package/dist/cliRunner.d.ts +20 -0
- package/dist/cliRunner.js +343 -0
- package/dist/cliRunner.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +204 -0
- package/dist/config.js.map +1 -0
- package/dist/convert.d.ts +20 -0
- package/dist/convert.js +61 -0
- package/dist/convert.js.map +1 -0
- package/dist/emitters/django.d.ts +4 -0
- package/dist/emitters/django.js +501 -0
- package/dist/emitters/django.js.map +1 -0
- package/dist/emitters/doctrine.d.ts +20 -0
- package/dist/emitters/doctrine.js +1134 -0
- package/dist/emitters/doctrine.js.map +1 -0
- package/dist/emitters/graphene.d.ts +4 -0
- package/dist/emitters/graphene.js +431 -0
- package/dist/emitters/graphene.js.map +1 -0
- package/dist/emitters/laravel.d.ts +33 -0
- package/dist/emitters/laravel.js +1695 -0
- package/dist/emitters/laravel.js.map +1 -0
- package/dist/emitters/prisma.d.ts +21 -0
- package/dist/emitters/prisma.js +1058 -0
- package/dist/emitters/prisma.js.map +1 -0
- package/dist/emitters/typeorm.d.ts +9 -0
- package/dist/emitters/typeorm.js +833 -0
- package/dist/emitters/typeorm.js.map +1 -0
- package/dist/emitters/typescriptInterfaces.d.ts +15 -0
- package/dist/emitters/typescriptInterfaces.js +350 -0
- package/dist/emitters/typescriptInterfaces.js.map +1 -0
- package/dist/formats.d.ts +80 -0
- package/dist/formats.js +212 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +48 -0
- package/dist/io.js +328 -0
- package/dist/io.js.map +1 -0
- package/dist/ir.d.ts +197 -0
- package/dist/ir.js +10 -0
- package/dist/ir.js.map +1 -0
- package/dist/naming.d.ts +11 -0
- package/dist/naming.js +88 -0
- package/dist/naming.js.map +1 -0
- package/dist/parsers/django.d.ts +14 -0
- package/dist/parsers/django.js +1161 -0
- package/dist/parsers/django.js.map +1 -0
- package/dist/parsers/doctrine.d.ts +12 -0
- package/dist/parsers/doctrine.js +1782 -0
- package/dist/parsers/doctrine.js.map +1 -0
- package/dist/parsers/laravel.d.ts +17 -0
- package/dist/parsers/laravel.js +2824 -0
- package/dist/parsers/laravel.js.map +1 -0
- package/dist/parsers/phpSyntax.d.ts +124 -0
- package/dist/parsers/phpSyntax.js +429 -0
- package/dist/parsers/phpSyntax.js.map +1 -0
- package/dist/parsers/prisma.d.ts +12 -0
- package/dist/parsers/prisma.js +964 -0
- package/dist/parsers/prisma.js.map +1 -0
- package/dist/parsers/pythonSyntax.d.ts +38 -0
- package/dist/parsers/pythonSyntax.js +120 -0
- package/dist/parsers/pythonSyntax.js.map +1 -0
- package/dist/parsers/typeorm.d.ts +12 -0
- package/dist/parsers/typeorm.js +1614 -0
- package/dist/parsers/typeorm.js.map +1 -0
- package/dist/parsers/typescriptSyntax.d.ts +57 -0
- package/dist/parsers/typescriptSyntax.js +200 -0
- package/dist/parsers/typescriptSyntax.js.map +1 -0
- package/dist/prismaOnlyConstructs.d.ts +7 -0
- package/dist/prismaOnlyConstructs.js +76 -0
- package/dist/prismaOnlyConstructs.js.map +1 -0
- package/dist/result.d.ts +20 -0
- package/dist/result.js +18 -0
- package/dist/result.js.map +1 -0
- package/dist/transforms.d.ts +13 -0
- package/dist/transforms.js +142 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +83 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ormbridge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,409 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ormbridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Convert ORM models between frameworks and languages, so one data model can be shared across your whole stack, from the command line or from code.
|
|
4
|
+
|
|
5
|
+
| Format | Read | Write | Notes |
|
|
6
|
+
| --------------------------- | :--: | :---: | ---------------------------------------------------------- |
|
|
7
|
+
| Django models | ✔ | ✔ | `models.py` files or `models/` packages |
|
|
8
|
+
| Prisma schema | ✔ | ✔ | All six providers; Prisma 7 header via the API |
|
|
9
|
+
| TypeORM entities | ✔ | ✔ | Decorator-based entity classes |
|
|
10
|
+
| Doctrine ORM entities (PHP) | ✔ | ✔ | PHP 8 attributes; writes one file per entity |
|
|
11
|
+
| Laravel (PHP) | ✔ | ✔ | Reads migrations plus Eloquent models; writes both |
|
|
12
|
+
| TypeScript interfaces | | ✔ | Plain interfaces and enums for front ends |
|
|
13
|
+
| Graphene (graphene-django) | | ✔ | GraphQL types, queries and mutations that pair with Django |
|
|
14
|
+
|
|
15
|
+
See [What is converted](#what-is-converted) for the field-level mapping and [docs/CONVERSION_MATRIX.md](docs/CONVERSION_MATRIX.md) for exactly what survives a round trip between the readable formats.
|
|
16
|
+
|
|
17
|
+
- **Static parsing, no runtime needed.** Source files are read with [tree-sitter](https://tree-sitter.github.io/), so there is no Python, Django, PHP, Composer or database to install or connect to.
|
|
18
|
+
- **Safe for existing databases.** The default `preserve` mode keeps your existing table and column names, so a new tool can sit on top of the database you already have.
|
|
19
|
+
- **Honest output.** Anything that cannot be represented produces a warning naming the model and field.
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
### | [Install](#install) | [Usage](#usage) | [What is converted](#what-is-converted) | [Limitations and warnings](#limitations-and-warnings) | [Programmatic API](#programmatic-api) |
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install --global ormbridge # or: npx ormbridge ...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Node.js 20 or newer.
|
|
32
|
+
|
|
33
|
+
[Back to Table of Contents](#table-of-contents)
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Django -> Prisma
|
|
39
|
+
ormbridge convert -i ./shop/models.py -o ./prisma/schema.prisma
|
|
40
|
+
|
|
41
|
+
# Whole project: finds models.py files and models/ packages (skips migrations, venvs, node_modules)
|
|
42
|
+
ormbridge convert -i ./backend -o ./prisma/schema.prisma
|
|
43
|
+
|
|
44
|
+
# Prisma -> Django
|
|
45
|
+
ormbridge convert -i ./prisma/schema.prisma -o ./shop/models.py --app-label shop
|
|
46
|
+
|
|
47
|
+
# TypeORM -> Prisma (a directory needs --from; every .ts file in it is read, entities are picked out)
|
|
48
|
+
ormbridge convert -i ./src/entities --from typeorm -o ./prisma/schema.prisma
|
|
49
|
+
|
|
50
|
+
# Doctrine (PHP 8 attributes) -> Prisma (every .php file is read; vendor/, var/ and *Test.php are skipped)
|
|
51
|
+
ormbridge convert -i ./src/Entity --from doctrine -o ./prisma/schema.prisma
|
|
52
|
+
|
|
53
|
+
# Laravel project root -> Prisma (reads database/migrations and app/; vendor/, storage/, tests/ and bootstrap/cache are skipped)
|
|
54
|
+
ormbridge convert -i ./my-laravel-app --from laravel -o ./prisma/schema.prisma
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Formats are inferred from file extensions (`.py` = Django, `.prisma` = Prisma, `.ts` = TypeORM), or set explicitly with `--from` / `--to`. Passing a directory does not infer the format, so add `--from typeorm` (or `--from doctrine`, `--from laravel`) when reading a folder of entities; `.php` is not inferred either. Run `ormbridge formats` to list every supported format, its file extensions, and whether it can be read, written, or both. Without `-o`, the result is printed to stdout. Warnings go to stderr.
|
|
58
|
+
|
|
59
|
+
| Flag | Default | Description |
|
|
60
|
+
| ------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `-i, --input <paths...>` | required (or config) | Files or directories to read. Several files are merged into one schema, so abstract base classes can live in another file. |
|
|
62
|
+
| `-o, --output <path>` | stdout | File to write. Parent directories are created. |
|
|
63
|
+
| `-f, --from <format>` | inferred | `django`, `prisma`, `typeorm`, `doctrine` or `laravel` |
|
|
64
|
+
| `-t, --to <format>` | inferred | `django`, `prisma`, `typeorm`, `doctrine` or `laravel` (both write a directory), `typescript` or `graphene` (output only) |
|
|
65
|
+
| `--naming <mode>` | `preserve` | `preserve` or `normalize` (see below) |
|
|
66
|
+
| `--provider <name>` | `postgresql` | Prisma datasource: `postgresql`, `mysql`, `sqlite`, `sqlserver`, `mongodb`, `cockroachdb`. Controls native types such as `@db.VarChar`. |
|
|
67
|
+
| `--no-header` | off | Omit the Prisma `generator` / `datasource` blocks (useful when pasting models into an existing schema). |
|
|
68
|
+
| `--app-label <name>` | directory name | Django app label used for default table names (`<app>_<model>`). |
|
|
69
|
+
| `--auto-field <type>` | `int` | Key type for Django models without an explicit primary key: `int` or `bigint`. |
|
|
70
|
+
| `--namespace <name>` | `App\Entity`, `App\Models` | PHP namespace of the Doctrine entities or Laravel models (`--to doctrine` / `--to laravel` only). Enums go in a sibling namespace. |
|
|
71
|
+
| `--dry-run` | off | Run the whole conversion and report what would be written, without touching the filesystem. |
|
|
72
|
+
| `--check` | off | Exit with code 3 if an output file is missing or differs from the conversion. Writes nothing. Needs an output path. |
|
|
73
|
+
| `--config <path>` | searched | Read settings from this JSON config file instead of searching for one. |
|
|
74
|
+
| `--no-config` | off | Ignore any config file. |
|
|
75
|
+
|
|
76
|
+
### Dry run and check
|
|
77
|
+
|
|
78
|
+
`--dry-run` converts everything exactly as a real run would and reports each file it would create or update (line count, size, and for an existing file a short diff summary such as `+3 -1 lines (first difference at line 12)`), plus all warnings. Nothing is written.
|
|
79
|
+
|
|
80
|
+
`--check` is for CI. It compares the conversion with the existing output and exits `0` when every file is up to date. If a file is missing or differs, it exits `3` and names the file:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
error: prisma/schema.prisma is out of date: +3 -1 lines (first difference at line 12).
|
|
84
|
+
Generated output is stale. Run "ormbridge convert" without --check to regenerate it.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Only the files the conversion produces are compared; extra files already in an output directory are ignored.
|
|
88
|
+
|
|
89
|
+
### Config file
|
|
90
|
+
|
|
91
|
+
Instead of repeating flags, put them in `ormbridge.config.json` (or `.ormbridgerc.json`). ormbridge looks in the current directory and each parent directory, and stops after the first directory that contains a `package.json` (or at the filesystem root). Use `--config <path>` to pick a file explicitly, or `--no-config` to ignore it. The file is plain JSON; no code is executed. Relative paths inside it resolve against the config file's directory, not the working directory.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"naming": "normalize",
|
|
96
|
+
"provider": "postgresql",
|
|
97
|
+
"conversions": [
|
|
98
|
+
{
|
|
99
|
+
"name": "prisma",
|
|
100
|
+
"input": "backend",
|
|
101
|
+
"output": "prisma/schema.prisma"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "types",
|
|
105
|
+
"input": "backend",
|
|
106
|
+
"to": "typescript",
|
|
107
|
+
"output": "web/src/models.ts"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Key | Type | Same as |
|
|
114
|
+
| ------------- | --------------------- | ---------------------------------------------------------------- |
|
|
115
|
+
| `input` | string or string list | `-i, --input` |
|
|
116
|
+
| `output` | string | `-o, --output` |
|
|
117
|
+
| `from` | string | `-f, --from` |
|
|
118
|
+
| `to` | string | `-t, --to` |
|
|
119
|
+
| `naming` | string | `--naming` (`preserve` or `normalize`) |
|
|
120
|
+
| `provider` | string | `--provider` |
|
|
121
|
+
| `header` | boolean | `--no-header` when `false` |
|
|
122
|
+
| `appLabel` | string | `--app-label` |
|
|
123
|
+
| `autoField` | string | `--auto-field` (`int` or `bigint`) |
|
|
124
|
+
| `namespace` | string | `--namespace` (Doctrine output) |
|
|
125
|
+
| `conversions` | list of objects | Several named conversions; each takes the keys above plus `name` |
|
|
126
|
+
|
|
127
|
+
Top-level keys are defaults for every entry in `conversions`. Flags override config values. With no `-i`, `ormbridge convert` runs every conversion in the list (and `-o` is rejected, since the conversions write to different paths); with `-i`, it runs a single conversion from the top-level settings. Unknown keys and bad values fail with a message naming the key, such as `"conversions[1].naming" must be "preserve" or "normalize" (got "weird")`.
|
|
128
|
+
|
|
129
|
+
To fail CI when generated files are stale:
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
# .github/workflows/ci.yml
|
|
133
|
+
- run: npx ormbridge convert --check
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Exit codes
|
|
137
|
+
|
|
138
|
+
| Code | Meaning |
|
|
139
|
+
| ---- | ---------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `0` | Success (with `--check`: every output is up to date). |
|
|
141
|
+
| `1` | A conversion failed: unreadable input, parse error, unwritable output. Other conversions still run. |
|
|
142
|
+
| `2` | Usage or config error: bad flag or value, unknown format, invalid config file. Nothing is converted. |
|
|
143
|
+
| `3` | `--check` found an output file that is missing or stale. |
|
|
144
|
+
|
|
145
|
+
### Multi-file output
|
|
146
|
+
|
|
147
|
+
Most formats produce one text, written to the `-o` file. A format can instead return several files (a map of relative path to text; the Doctrine and Laravel formats do this). Then `-o` must be a directory (for example `ormbridge convert -i ./models.py --to doctrine -o ./`, or `--to laravel -o ./` to write `app/` and `database/migrations/` into a Laravel project): every file is written beneath it, creating folders as needed, and `--dry-run` and `--check` cover each file. If `-o` is an existing file or looks like a file path (it has an extension), ormbridge fails with a message listing the files and asking for a directory. See "Emitter output" under [Format registry](#format-registry) for the adapter side.
|
|
148
|
+
|
|
149
|
+
[Back to Table of Contents](#table-of-contents)
|
|
150
|
+
|
|
151
|
+
### Naming modes
|
|
152
|
+
|
|
153
|
+
**`preserve`** (default) keeps the existing database working unchanged: Django table names (`blog_post`) are kept via `@@map`, columns keep their names, and each Django many-to-many field becomes an explicit join model that matches the table Django already created.
|
|
154
|
+
|
|
155
|
+
**`normalize`** produces a fresh-schema style: singular snake_case table names (`post`), UUID primary keys in place of auto-increment ids, camelCase Prisma fields mapped to snake_case columns, `created_at` / `updated_at` added to models that lack them, and implicit many-to-many relations.
|
|
156
|
+
|
|
157
|
+
## What is converted
|
|
158
|
+
|
|
159
|
+
| Django | Prisma | TypeORM | Doctrine |
|
|
160
|
+
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
161
|
+
| `CharField`, `SlugField`, `EmailField`, `URLField` | `String @db.VarChar(n)` | `@Column({ type: 'varchar', length: n })` | `type: 'string'`, `length: n` |
|
|
162
|
+
| `TextField` | `String @db.Text` | `@Column({ type: 'text' })` | `type: 'text'` |
|
|
163
|
+
| `Integer*`, `PositiveInteger*` / `BigInteger*` / `AutoField` / `BigAutoField` | `Int` / `BigInt` / `@default(autoincrement())` | `int` / `bigint` (`string`), `@PrimaryGeneratedColumn('increment')` | `integer` / `bigint` (`string`), `#[ORM\GeneratedValue(strategy: 'AUTO')]` |
|
|
164
|
+
| `FloatField`, `DecimalField` | `Float`, `Decimal @db.Decimal(p, s)` | `float`, `decimal` with `precision` / `scale` (`string`) | `float`, `decimal` with `precision` / `scale` (`string`) |
|
|
165
|
+
| `BooleanField`, `UUIDField`, `JSONField`, `BinaryField` | `Boolean`, `String @db.Uuid`, `Json`, `Bytes` | `boolean`, `uuid`, `jsonb`, `bytea` (types follow `--provider`) | `boolean`, `guid`, `json` (`jsonb` on PostgreSQL), `blob` |
|
|
166
|
+
| `DateTimeField`, `DateField`, `TimeField` | `DateTime` (with `@db.Date` / `@db.Time`) | `timestamptz` / `date` / `time` | `DateTimeImmutable` (`datetimetz_immutable` on PostgreSQL, `date_immutable`, `time_immutable`) |
|
|
167
|
+
| `ArrayField(base_field)` (nested arrays too) | scalar list `String[]` (PostgreSQL, CockroachDB, MongoDB) | `array: true` on PostgreSQL, `json` elsewhere | `json` (PHP `array`) plus a warning |
|
|
168
|
+
| `HStoreField` | `Json` | `hstore` on PostgreSQL, `json` elsewhere | `json` plus a warning |
|
|
169
|
+
| `IntegerRangeField`, `BigIntegerRangeField`, `DecimalRangeField`, `DateRangeField`, `DateTimeRangeField` | `Unsupported("int4range")` and friends on PostgreSQL, `String` elsewhere | `int4range`, `int8range`, `numrange`, `daterange`, `tstzrange` on PostgreSQL | `string` plus a warning |
|
|
170
|
+
| `GenericIPAddressField`, `IPAddressField` | `String @db.Inet` (PostgreSQL) | `inet` on PostgreSQL, `varchar(45)` elsewhere | `string` (length 45) |
|
|
171
|
+
| `DurationField` | `BigInt` (no interval type) | `interval` on PostgreSQL, `bigint` elsewhere | `dateinterval` plus a warning |
|
|
172
|
+
| `FileField`, `ImageField`, `FilePathField`, `SmallIntegerField`, `PositiveSmallIntegerField`, `SmallAutoField` | `String`, `Int` as above | `varchar(100)`, `int` as above | `string(100)`, `integer` as above |
|
|
173
|
+
| `GeneratedField` (`expression`, `output_field`, `db_persist`) | regular column plus a warning | regular column plus a warning | regular column plus a warning |
|
|
174
|
+
| `db_default` (`Value(...)`, `Now()`, literals) | `@default(...)` | `default: ...` | `options: ['default' => ...]` |
|
|
175
|
+
| `auto_now_add` / `auto_now` | `@default(now())` / `@updatedAt` | `@CreateDateColumn` / `@UpdateDateColumn` | constructor default plus `#[ORM\PrePersist]` / `#[ORM\PreUpdate]` callbacks |
|
|
176
|
+
| `default=uuid.uuid4`, `timezone.now`, literals | `@default(uuid())`, `@default(now())`, literals | `@PrimaryGeneratedColumn('uuid')`, `default: () => 'CURRENT_TIMESTAMP'`, literals | constructor-generated UUID, `options: ['default' => ...]`, literals |
|
|
177
|
+
| `TextChoices`, inline `choices=[...]` | `enum` | TypeScript `enum` plus `@Column({ type: 'enum', enum })` | backed `enum` in `src/Enum/` plus `enumType:` |
|
|
178
|
+
| `ForeignKey`, `OneToOneField` (`on_delete`, `related_name`, `null`) | `@relation(... onDelete: ...)` plus the reverse field | `@ManyToOne` / `@OneToOne` with `@JoinColumn` and `onDelete`, plus `@OneToMany` / inverse `@OneToOne` | `#[ORM\ManyToOne]` / `#[ORM\OneToOne]` with `#[ORM\JoinColumn(onDelete:)]`, plus `OneToMany(mappedBy:)` / inverse `OneToOne` |
|
|
179
|
+
| `ManyToManyField` | join model (`preserve`) or implicit relation (`normalize`) | `@ManyToMany` with `@JoinTable` and the inverse side | `#[ORM\ManyToMany]` with `#[ORM\JoinTable]` and the inverse side |
|
|
180
|
+
| `Meta.db_table`, `unique_together`, `indexes`, `UniqueConstraint`, `db_index` | `@@map`, `@@unique`, `@@index` | `@Entity('table')`, `@Unique([...])`, `@Index([...])` | `#[ORM\Table(name:)]`, `#[ORM\Index]`, `#[ORM\UniqueConstraint]` |
|
|
181
|
+
| Abstract base classes, multi-table inheritance | fields inherited / one-to-one primary key | fields inherited / one-to-one primary key | fields inherited / `#[ORM\Id]` on the one-to-one |
|
|
182
|
+
| Proxy models (`Meta.proxy`), `Meta.swappable`, `models.CompositePrimaryKey` | merged into the concrete model / resolved / `@@id` | merged into the concrete model / resolved / composite `@PrimaryColumn` | merged into the concrete model / resolved / several `#[ORM\Id]` |
|
|
183
|
+
|
|
184
|
+
TypeORM output is a single TypeScript file of entity classes. Relations use `Relation<T>` so entities declared in one file do not trip over circular references, and `--provider` picks the column types (`timestamptz` and `jsonb` for PostgreSQL, `datetime` and `json` for MySQL, `simple-json` for SQLite). Use `--naming normalize` for camelCase properties mapped to snake_case columns with `name:` options. `.ts` is not inferred as a format, so pass `--from typeorm` or `--to typeorm`. Reading uses decorators only (see Limitations). TypeORM entities are parsed statically with tree-sitter (no `reflect-metadata`, TypeScript compiler, or database needed), and the same naming modes apply to the result.
|
|
185
|
+
|
|
186
|
+
Doctrine (`--from doctrine`) reads PHP 8 entity classes with the tree-sitter PHP grammar, so no PHP, Composer or database is needed. Pass the entity directory (one class per file is typical; enums and mapped superclasses can live in other files) or individual files; `.php` is not claimed because Laravel uses it too. Doctrine's default underscore naming is assumed for anything the attributes leave out: the table is the snake_cased class name, the column the snake_cased property, a join column `<property>_id`, an embedded column `<property>_<column>`.
|
|
187
|
+
|
|
188
|
+
| Doctrine (PHP 8 attributes) | Read as |
|
|
189
|
+
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
+
| `#[ORM\Entity]` (`repositoryClass` ignored), `#[ORM\Table(name:)]` | a model; table `name` or the snake_cased class name |
|
|
191
|
+
| `#[ORM\Column]` `type` (or a `Types::` constant), `length`, `precision`, `scale`, `nullable`, `unique`, `name` | string (length 255 by default), text, int, bigint, decimal (10,0 by default), float, boolean, date and time types, json, binary, guid |
|
|
192
|
+
| `#[ORM\Column]` without `type` | inferred from the PHP type, as Doctrine does (`int`, `string`, `bool`, `float`, `array`, `DateTime`, `DateTimeImmutable`, `DateInterval`, backed enums) |
|
|
193
|
+
| `options: ['default' => ...]` | literal, `now` (`CURRENT_TIMESTAMP`), `uuid` (`gen_random_uuid()`) or enum member defaults |
|
|
194
|
+
| `#[ORM\Id]`, `#[ORM\GeneratedValue(strategy:)]`, `#[ORM\CustomIdGenerator]` | primary key; AUTO/IDENTITY/SEQUENCE become auto-increment, UUID (and Symfony `UuidGenerator`) a UUID key, NONE no default |
|
|
195
|
+
| several `#[ORM\Id]` | composite primary key |
|
|
196
|
+
| `#[ORM\ManyToOne]`, `#[ORM\OneToOne]` with `#[ORM\JoinColumn(name:, referencedColumnName:, nullable:, onDelete:)]` | foreign key / one-to-one (column `<property>_id`, nullable unless `nullable: false`) |
|
|
197
|
+
| `#[ORM\OneToMany]`, inverse `#[ORM\OneToOne]`, `mappedBy` / `inversedBy` | the reverse accessor of the owning relation |
|
|
198
|
+
| `#[ORM\ManyToMany]` (owning side) | many-to-many (custom `#[ORM\JoinTable]` names are not preserved) |
|
|
199
|
+
| `#[ORM\Index]`, `#[ORM\UniqueConstraint]` (class level or inside `#[ORM\Table]`), `Column(unique:, index:)` | indexes, unique constraints (a single unnamed unique column becomes a unique field), `fulltext` flag |
|
|
200
|
+
| backed enums (`enum Status: string`) used through `enumType` or the property type | an enum plus an enum-backed column |
|
|
201
|
+
| `#[ORM\Embeddable]` / `#[ORM\Embedded(columnPrefix:)]` | the embeddable's columns flattened into the owner, prefixed as Doctrine does |
|
|
202
|
+
| `#[ORM\MappedSuperclass]` and traits | columns, relations and indexes inherited |
|
|
203
|
+
| `#[ORM\InheritanceType('SINGLE_TABLE')]`, `#[ORM\DiscriminatorColumn]` | subclasses merged into the root table (nullable columns) plus a discriminator column |
|
|
204
|
+
| `#[ORM\InheritanceType('JOINED')]` | child table with a one-to-one primary key to the parent |
|
|
205
|
+
| `#[ORM\Version]` | integer column defaulting to 1 (optimistic locking is not represented) |
|
|
206
|
+
| `#[ORM\HasLifecycleCallbacks]`, lifecycle callbacks, `#[ORM\Cache]`, `#[ORM\OrderBy]`, other libraries' attributes | ignored without a warning |
|
|
207
|
+
|
|
208
|
+
Doctrine output (`--to doctrine`) writes a directory, so pass `-o <directory>`. It follows PSR-4: one PHP 8.1+ file per entity (`src/Entity/Post.php`) and one backed enum per enum (`src/Enum/PostStatus.php`), each with `declare(strict_types=1);` and `use Doctrine\ORM\Mapping as ORM;`. `.php` is not inferred, so pass `--to doctrine`.
|
|
209
|
+
|
|
210
|
+
- **Namespace and paths.** The default namespace is `App\Entity` (enums in `App\Enum`). `--namespace Acme\Blog\Entity` (or `namespace` in the config file, or the `namespace` option of `emitDoctrine` / `convertText`) writes `src/Blog/Entity/Post.php` and puts the enums in `Acme\Blog\Enum`: the first namespace segment maps to `src/`, the rest to folders, and a namespace not ending in `Entity` gets a child `Enum` namespace.
|
|
211
|
+
- **Properties.** Typed and nullable (`?int`, `?string`, `DateTimeImmutable`, `bool`), private, with explicit `type:`, `length:`, `precision:` / `scale:`, `nullable:`, `unique:` and the column `name:`. Defaults are PHP property initializers plus `options: ['default' => ...]`; `now` defaults and UUIDs are set in the constructor (no extra packages: UUID keys and defaults use a small private `generateUuid()` helper). Auto-updated timestamps get `#[ORM\PrePersist]` / `#[ORM\PreUpdate]` lifecycle callbacks. Enum columns use `enumType:` with the PHP backed enum, and a default becomes the enum case.
|
|
212
|
+
- **Keys.** An auto-increment key is `#[ORM\Id]` with `#[ORM\GeneratedValue(strategy: 'AUTO')]`; composite keys repeat `#[ORM\Id]`, and a relation that is the primary key (multi-table inheritance) carries the `#[ORM\Id]` itself.
|
|
213
|
+
- **Relations.** Every relation is written on both sides with matching `inversedBy:` / `mappedBy:`: `ManyToOne` with `JoinColumn(name:, referencedColumnName:, nullable:, onDelete:)` and an inverse `OneToMany`, `OneToOne` with an inverse `OneToOne`, and `ManyToMany` with `JoinTable`, `JoinColumn`, `InverseJoinColumn` and an inverse `ManyToMany`. To-many properties are `Collection` (docblock `Collection<int, Post>`) initialised with `ArrayCollection` in the constructor.
|
|
214
|
+
- **Accessors.** Getters and fluent `static` setters (`isFeatured()` for booleans); to-many properties get `getPosts()`, `addPost()` and `removePost()` that keep the owning and inverse sides in sync.
|
|
215
|
+
- **Tables and indexes.** `#[ORM\Table(name:)]` plus class-level `#[ORM\Index(columns: [...])]` and `#[ORM\UniqueConstraint(columns: [...])]` on database column names. (`Table(indexes:, uniqueConstraints:)` has no effect in Doctrine ORM 3, so it is not used; ORM 2.19 or newer and PHP 8.1 or newer are required.) Reserved SQL words used as table or column names (`user`, `order`, ...) are written with Doctrine's backtick quoting.
|
|
216
|
+
- **Naming.** `preserve` keeps the property names of the source (`published_at`) and always writes the table and column names explicitly; `normalize` writes camelCase properties (`publishedAt`) mapped to snake_case columns. Getter and setter names are always camelCase (`getPublishedAt()`).
|
|
217
|
+
- **Lossy or unsupported.** Arrays and HStore become `json` columns; ranges become strings; durations become `dateinterval` (an ISO 8601 string); `GeneratedField` expressions, views, composite foreign keys, `Unsupported(...)` types, full-text indexes, `@@schema` and other Prisma-only constructs are written as plain tables and columns; a relation to a model without a single-column primary key is skipped; each case warns with the model and field. `onUpdate` actions have no Doctrine equivalent. A relation that points at a non-primary-key column (`to_field`) is written, but Doctrine's `orm:validate-schema` rejects it, and the warning says so. Doctrine ORM is relational, so `--provider mongodb` only warns.
|
|
218
|
+
- **Reading.** `--from doctrine` reads the same attributes back, so Doctrine round trips through the shared model.
|
|
219
|
+
|
|
220
|
+
Laravel output (`--to laravel`) writes a directory, so pass `-o <directory>` (the project root of a Laravel 11 or 12 app, for example `-o .`). `.php` is not inferred, so pass `--to laravel` (and `--from laravel` to read a project back, described below). Existing files with the same name are overwritten (use `--check` or `--dry-run` first); the migration file names are fixed (see below), so running the conversion again rewrites the same files rather than adding new ones.
|
|
221
|
+
|
|
222
|
+
- **Files.** One migration per table in `database/migrations/` (anonymous `return new class extends Migration` with `up()` and `down()` = `Schema::dropIfExists`), one Eloquent model per model in `app/Models/` (namespace `App\Models`) and one backed enum per enum in `app/Enums/` (`App\Enums`). `--namespace Acme\Blog\Models` (or `namespace` in the config file, or the `namespace` option of `emitLaravel` / `convertText`) writes `app/Blog/Models/Post.php` and puts the enums in `Acme\Blog\Enums`; the first namespace segment maps to `app/`.
|
|
223
|
+
- **Migration order and names.** Files are named `2024_01_01_000001_create_posts_table.php`, `..._000002_...`, counting up by one, with the date fixed so the output is deterministic. Tables are ordered so a foreign key always points at a table created earlier. A self reference is written inline (`constrained()` on the same table). A real dependency cycle (A needs B, B needs A) creates the tables first and adds the constraints that could not be written in a later `add_foreign_keys_to_<table>_table` migration, whose `down()` drops them. A fresh Laravel app already has a `users` migration, so a `User` model produces a second `create_users_table`: delete one of them.
|
|
224
|
+
- **Columns.** `id()` for an auto-increment key (`bigIncrements('code')` when the key is not called `id`; an `int` key becomes a big integer, like Laravel's own `id()`, and foreign keys use `foreignId`), `uuid()->primary()` or `ulid()->primary()` only when the schema says so (with `HasUuids` / `HasUlids` and `foreignUuid` / `foreignUlid` references), `string(n)`, `text`, `integer`, `bigInteger`, `unsignedInteger` and the other sized integers when a Prisma native type (`@db.UnsignedInt`, ...) says so, `double`, `decimal(p, s)`, `boolean`, `dateTime` / `dateTimeTz` / `date` / `time`, `jsonb` on PostgreSQL and `json` elsewhere, `binary`, `ipAddress`, `enum('status', [...])`, `nullable()`, `unique()`, `default(...)` and `useCurrent()` for `now` defaults. A `created_at` + `updated_at` pair becomes `timestamps()` (Eloquent maintains it) and a nullable `deleted_at` becomes `softDeletes()` with `use SoftDeletes`. Indexes and unique constraints are `index([...], 'name')` / `unique([...])`, and composite keys `primary([...])`.
|
|
225
|
+
- **Foreign keys.** `foreignId('author_id')->constrained('auth_user')->cascadeOnDelete()` with the matching helper for each action (`restrictOnDelete()`, `nullOnDelete()`, `cascadeOnUpdate()`, ...; `set default` through `onDelete('set default')`). A reference to a non-id column or to a string key is written as `$table->foreign('col')->references('col')->on('table')`.
|
|
226
|
+
- **Many-to-many.** A pivot table with both foreign keys and a composite primary key. `normalize` follows Laravel's convention (`post_tag`: singular model names in alphabetical order, `post_id` and `tag_id`) so `belongsToMany(Tag::class)` needs no extra arguments; `preserve` keeps the `<table>_<relation>` name of the other formats and passes the table and key names explicitly. A self relation uses `from_user_id` / `to_user_id`. When the source already models the join table (Prisma's explicit join model), it is an ordinary table and model with `belongsTo` / `hasMany`.
|
|
227
|
+
- **Models.** `$table`, `$primaryKey`, `$keyType`, `$incrementing` and `$timestamps` are written only when they differ from what Laravel would guess (`$table` is also written when the plural of the model name is not certain: `Person` is fine, `Axis` is written out). `$fillable` lists every writable column except generated keys and `timestamps()` columns; `$casts` has backed enums, `boolean`, `array` (json columns, arrays, hstore), `datetime`, `date`, `float` and `decimal:N`. Every relation is written on both sides with typed methods and generics docblocks: `belongsTo` (and `hasMany` / `hasOne` on the target), `belongsToMany` on both models, with explicit keys only when they are not Laravel's (`belongsTo(User::class, 'writer_user_id')`). A method that would clash with a column or an Eloquent method gets a `Relation` suffix and a warning.
|
|
228
|
+
- **Naming.** `preserve` keeps table and column names exactly (written as `$table` and in the migrations); `normalize` uses plural snake_case tables (`blog_posts`), snake_case columns, `<relation>_id` keys, adds `created_at` / `updated_at` where missing and keeps auto-increment keys as they are. Relationship methods are always camelCase (`editedPosts()`).
|
|
229
|
+
- **Lossy or unsupported.** Arrays and HStore become `json` columns, ranges and durations become strings, `GeneratedField` expressions (Python, not SQL), views, composite foreign keys, `Unsupported(...)` types, full-text indexes, index options, `@@schema` and other Prisma-only constructs are written as plain tables and columns; a relation to a model without a single-column primary key is skipped; each case warns with the model and field. Eloquent has no composite primary keys: the migration gets `primary([...])` and the model a warning. An auto-updated column that is not part of `timestamps()` is only refreshed by MySQL (`useCurrentOnUpdate()`). Laravel does not map PostgreSQL-specific types, so `--provider` only chooses `jsonb` versus `json` and the MySQL form of JSON defaults.
|
|
230
|
+
- **Not converted.** Policies, factories, seeders, observers, morph relations and anything else outside the schema are not generated, and the models extend `Illuminate\Database\Eloquent\Model` (not `Authenticatable`).
|
|
231
|
+
|
|
232
|
+
Graphene output (`--to graphene`) is write-only and pairs with Django models: it writes one Python file that imports every model from `.models` (generate it with `--to django`, or use your own models) and defines a `DjangoObjectType` per model with an explicit `Meta.fields` list (forward and reverse relations included), a `Query` with a single-item field (`post(id)`) and a list field (`post_list`) per model, `Create`/`Update`/`Delete` mutations built on `graphene.Mutation`, and a final `schema = graphene.Schema(query=Query, mutation=Mutation)`. Inputs map the scalar types to `String`, `Int`, `BigInt`, `Float`, `Decimal`, `Boolean`, `DateTime`, `Date`, `Time`, `UUID` and `JSONString`; a field is required when it is not nullable and has no default, and relations are `ID` inputs (`author_id`, many-to-many `tags_ids`). Enum-backed fields rely on graphene-django's choice conversion for output and are `String` inputs validated by the model. Binary columns and relations to models outside the schema are left out, and models with a composite primary key get only a list query and a create mutation; each case produces a warning naming the model and field. Field names are snake_case like the Django output, `.py` stays owned by Django so pass `--to graphene`, and the naming mode has no effect. Requires graphene-django 3.x at runtime.
|
|
233
|
+
|
|
234
|
+
Django-only constructs: a proxy model (`Meta.proxy = True`) has no table of its own, so it is merged into its concrete model (chains of proxies are followed) and relations that target it are pointed at the concrete model, with a warning. A model with `Meta.swappable = "AUTH_USER_MODEL"` is used as the target of `settings.AUTH_USER_MODEL` and `get_user_model()` instead of an assumed `User`. Custom managers (`objects = MyManager()`, `Manager.from_queryset(...)()`, `QuerySet.as_manager()`) and the classes behind them are ignored without warnings. `django.contrib.postgres` arrays, HStore and range fields, and `GeneratedField` / `db_default` (Django 5.0+), round-trip through `--to django`; the Django emitter writes the `django.contrib.postgres.fields` import and notes the required Django version in a warning.
|
|
235
|
+
|
|
236
|
+
Prisma → Django applies the reverse mapping. Field names are converted from camelCase to snake_case, with `db_column` set when the column name differs.
|
|
237
|
+
|
|
238
|
+
### Laravel (`--from laravel`)
|
|
239
|
+
|
|
240
|
+
Laravel keeps its schema in migrations, not in the models, so `--from laravel` works in two steps, with the tree-sitter PHP grammar (no PHP, Composer or database is needed). First it replays the migration files in order (sorted by file name, which starts with the timestamp) to rebuild the **current** schema: a column added by one migration, renamed by the next and dropped by a third is handled the way the database would. Then it reads the Eloquent models for what the migrations do not say: relation names and their reverse names, casts to backed enums, custom table names and timestamp handling. A table without a model is still converted (its model is named after the singular table name), and `down()` is ignored.
|
|
241
|
+
|
|
242
|
+
Pass the project root (only `database/migrations` and `app/` are read; `vendor/`, `storage/`, `tests/`, `bootstrap/cache` and `node_modules/` are skipped), or the migrations and model directories separately: `ormbridge convert -i ./database/migrations ./app/Models ./app/Enums --from laravel`. Files are recognised by what they contain, not by where they live: a class whose `up()` calls `Schema::`, a class that extends `Model` (or `Authenticatable`, or another model) and a backed enum, so a flat folder works too. Anonymous (`return new class extends Migration`) and named migration classes are both read. `.php` is not claimed, so pass `--from laravel`. Laravel output (`--to laravel`) is described above.
|
|
243
|
+
|
|
244
|
+
| Laravel migration | Read as |
|
|
245
|
+
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
246
|
+
| `Schema::create`, `Schema::table`, `drop`, `dropIfExists`, `rename`, `dropColumns` | tables are created, altered, dropped and renamed in migration order |
|
|
247
|
+
| `$table->dropColumn`, `renameColumn`, `->change()` | the column is removed, renamed (also inside indexes and keys) or restated (`change()` replaces the definition, as Laravel does) |
|
|
248
|
+
| `dropIndex`, `dropUnique`, `dropFullText`, `dropPrimary`, `dropForeign`, `renameIndex` | by name or by column list, using Laravel's default index names (`posts_title_index`) |
|
|
249
|
+
| `dropTimestamps`, `dropSoftDeletes`, `dropRememberToken`, `dropMorphs`, `dropConstrainedForeignId` | the columns they added are removed |
|
|
250
|
+
| `id`, `bigIncrements`, `increments` and the tiny / small / medium variants, `integer(col, true)` | auto-increment primary key (`BigInt` for `id` and `bigIncrements`) |
|
|
251
|
+
| `string`, `char`, `text`, `mediumText`, `longText`, `tinyText` | `string` with its length (255 by default), `text` |
|
|
252
|
+
| `integer`, `tinyInteger`, `smallInteger`, `mediumInteger`, `bigInteger` and the `unsigned*` variants, `year` | `int` / `bigInt` (unsigned is not tracked) |
|
|
253
|
+
| `boolean`, `decimal(col, total, places)`, `float`, `double`, `unsignedDecimal` | `boolean`, `decimal` with precision and scale (8, 2 by default), `float` |
|
|
254
|
+
| `date`, `dateTime`, `dateTimeTz`, `time`, `timeTz`, `timestamp`, `timestampTz` | `date`, `dateTime`, `time` |
|
|
255
|
+
| `timestamps`, `timestampsTz`, `nullableTimestamps`, `softDeletes`, `softDeletesTz`, `rememberToken` | expanded to `created_at` / `updated_at` (nullable), `deleted_at` (nullable), `remember_token` (`string(100)`, nullable) |
|
|
256
|
+
| `json`, `jsonb`, `binary`, `uuid`, `ulid`, `ipAddress` | `json`, `bytes`, `uuid`, `string(26)`, `ipAddress` |
|
|
257
|
+
| `enum(col, [...])` | a string column with an enum; a cast to a backed enum on the model supplies the enum name, otherwise one is created from the values (`PostStatus`) |
|
|
258
|
+
| `nullable`, `default` (literals, `DB::raw`, `now()`, `Carbon::now()`), `useCurrent`, `useCurrentOnUpdate`, `storedAs` / `virtualAs` | nullability, defaults (`CURRENT_TIMESTAMP` becomes `now`, `gen_random_uuid()` becomes a UUID default), auto-updated timestamps, generated columns |
|
|
259
|
+
| `unique`, `index`, `fullText`, `primary` (also with column lists and names), `->unique()` and `->index()` on a column | unique columns, indexes (a name is kept only when it is not Laravel's default), composite primary keys |
|
|
260
|
+
| `foreignId()->constrained()`, `foreignUuid`, `foreignUlid`, `foreignIdFor`, `foreign()->references()->on()`, `onDelete`, `onUpdate` | a relation with its column, nullability and referential actions (`cascadeOnDelete`, `restrictOnDelete`, `nullOnDelete`, `noActionOnDelete` and the update variants) |
|
|
261
|
+
| `comment`, `after`, `first`, `charset`, `collation`, `engine`, `invisible` | ignored without a warning |
|
|
262
|
+
|
|
263
|
+
| Eloquent model | Read as |
|
|
264
|
+
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
265
|
+
| `class Post extends Model` (also `Authenticatable`, abstract base models, `Pivot`) | the table is `$table`, or Laravel's convention: the snake_case plural of the class name (`UserProfile` becomes `user_profiles`); a model without a table is skipped with a warning |
|
|
266
|
+
| `$casts` / `casts()` to a string-backed enum (PHP 8.1), `boolean`, `array` / `json`, `decimal:2`, `datetime` | the field gets the enum (declared in any file), and the types the cast implies |
|
|
267
|
+
| `$timestamps`, `CREATED_AT`, `UPDATED_AT` | with timestamps on, the update column refreshes on every save and the creation column starts at the current time; with `$timestamps = false` nothing is assumed |
|
|
268
|
+
| `use SoftDeletes`, `HasUuids`, `HasUlids` | `deleted_at` is checked to exist; the primary key gets a generated UUID / ULID default |
|
|
269
|
+
| `belongsTo`, `hasMany`, `hasOne` (custom foreign and owner keys) | name the relation (`author`) and its reverse (`posts`); the foreign key is `<method>_id` for `belongsTo` and `<model>_id` for `hasOne` / `hasMany`, as in Laravel; a `hasOne` or a unique column makes it one-to-one |
|
|
270
|
+
| `belongsToMany` (pivot table, key and `withPivot` arguments) | a pivot table that holds only the two keys (and optionally an `id` and timestamps) becomes a many-to-many field; the default pivot name is the singular model names in alphabetical order (`post_tag`); a pivot with other columns stays a model |
|
|
271
|
+
|
|
272
|
+
Everything is taken from the migrations and the models together, so a relation can come from a foreign key constraint, from a `belongsTo`, or from the `hasMany` on the other side; a foreign key column with none of the three is a plain column.
|
|
273
|
+
|
|
274
|
+
### Prisma constructs
|
|
275
|
+
|
|
276
|
+
The Prisma parser and emitter cover more than the Django mapping above. Constructs the shared model can carry survive a Prisma → Prisma round trip; the other formats get the closest equivalent and a warning naming the model and field.
|
|
277
|
+
|
|
278
|
+
| Prisma | Read as | Written back / other formats |
|
|
279
|
+
| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
280
|
+
| `@relation(fields: [a, b], references: [x, y])` (composite foreign key, `onDelete`, `onUpdate`, `map:`) | composite foreign key; the columns stay plain fields | written back with the reverse field; dropped with a warning in Django, TypeORM, TypeScript and Graphene |
|
|
281
|
+
| `@@id([a, b], map: "...")`, `@id(map: "...")`, `@unique(map: "...")`, `@@index(..., map: "...")`, `@relation(map: "...")` | constraint names | written back where the provider supports them (primary key names are dropped with a warning on MySQL, SQLite and MongoDB, foreign key names on SQLite and MongoDB) |
|
|
282
|
+
| `@@index([a(sort: Desc, length: 10, ops: ...)], type: Hash, clustered: ...)` | index options | written back where the provider accepts them (`type` on PostgreSQL, `Gin` also on CockroachDB; `length` on MySQL; `ops` on PostgreSQL; `clustered` on SQL Server), otherwise dropped with a warning |
|
|
283
|
+
| `@@fulltext([a, b])` | full-text index | `@@fulltext` on MySQL and MongoDB; an ordinary `@@index` with a warning elsewhere |
|
|
284
|
+
| `view` blocks | model flagged as a view | `view` block (the header gets `previewFeatures = ["views"]`); other formats treat it as a table and warn |
|
|
285
|
+
| `Unsupported("circle")` | opaque column (`Unsupported("int4range")` and friends become range fields) | `Unsupported("circle")` plus a warning that Prisma Client cannot use it; a plain string column and a warning elsewhere |
|
|
286
|
+
| `@db.*` native types (PostgreSQL, CockroachDB, MySQL, SQL Server, MongoDB) | carried on the field | written back when the target provider accepts that type for the scalar, otherwise dropped with a warning in favour of the provider default |
|
|
287
|
+
| `@default(cuid())`, `ulid()`, `nanoid(n)`, `uuid(7)`, `dbgenerated("...")`, `auto()`, `sequence()` | kept as defaults | written back; dropped with a warning in the other formats (`uuid()` still maps to a UUID default) |
|
|
288
|
+
| `String[]`, `Int[]`, ... (scalar lists), `@default([])` | array fields | scalar list on PostgreSQL, CockroachDB and MongoDB, `Json` elsewhere; `ArrayField` in Django, `array: true` in TypeORM |
|
|
289
|
+
| `@@schema("...")` with the datasource `schemas` list (multiSchema), `@@map` on enums | schema and database name | written back with the `schemas` list on PostgreSQL, CockroachDB and SQL Server, dropped with a warning elsewhere |
|
|
290
|
+
| `@ignore`, `@@ignore` | flags | written back; dropped with a warning in the other formats |
|
|
291
|
+
|
|
292
|
+
The Prisma emitter also adapts to what the target provider rejects, each time with a warning naming the model and field: `Restrict` becomes `NoAction` on SQL Server, enums are written as `String` on SQL Server (which has none), `autoincrement()` on an `Int` becomes `sequence()` on CockroachDB, `Json` is written as `String @db.NVarChar(Max)` on SQL Server, and CockroachDB uses its own type names (`@db.String(n)`, not `@db.VarChar(n)`).
|
|
293
|
+
|
|
294
|
+
Prisma 7 changed the schema header: the `prisma-client` generator replaces `prisma-client-js` and needs an `output` path, and the datasource no longer takes `url` (connection URLs move to `prisma.config.ts`). Programmatic callers can pass `prismaVersion: 7` to `convertText` (or `emitPrisma`) to get that header; the default stays Prisma 6, and there is no CLI flag for it yet. ormbridge does not generate `prisma.config.ts` or the driver adapter that Prisma 7 needs at runtime.
|
|
295
|
+
|
|
296
|
+
### TypeScript interfaces (`--to typescript`)
|
|
297
|
+
|
|
298
|
+
Writes plain TypeScript interfaces and enums for sharing models with a front end (for example an Angular app). It is output only, needs no parser, and `.ts` is not inferred, so pass `--to typescript`.
|
|
299
|
+
|
|
300
|
+
- One `export interface` per model, with properties in the IR's order, and one string `export enum` per enum using the stored values (a Django choice label becomes a `/** ... */` comment). The IR carries no model or field help text, so no other doc comments are written.
|
|
301
|
+
- Types follow what travels as JSON: `string`, `text`, `uuid`, `time`, `bigInt`, `decimal` (to keep precision) and `bytes` (base64) are `string`; `int` and `float` are `number`; `boolean` is `boolean`; `json` is `unknown`. `DateTime` and `Date` are `string` (ISO 8601). Programmatic callers can pass `dates: 'date'` to `emitTypescriptInterfaces` to type them as `Date`; the CLI always writes `string`.
|
|
302
|
+
- Nullable fields get `| null`. Fields are never optional, because the interfaces describe the data a server returns, defaults included.
|
|
303
|
+
- A foreign key is written as its scalar (`categoryId: number`, typed from the referenced primary key or `to_field`) plus an optional expanded relation (`category?: Category`). Reverse sides are optional (`comments?: Comment[]`, a one-to-one reverse side is `profile?: Profile | null`), as are many-to-many lists on both sides.
|
|
304
|
+
- `--naming normalize` writes camelCase property names, `preserve` keeps the existing names. Names that are not valid identifiers are quoted.
|
|
305
|
+
- Warnings name the model and field for relations to unknown models, composite-key targets (the foreign key becomes `unknown`), unknown enums or field types, and invalid or clashing type names.
|
|
306
|
+
|
|
307
|
+
[Back to Table of Contents](#table-of-contents)
|
|
308
|
+
|
|
309
|
+
## Limitations and warnings
|
|
310
|
+
|
|
311
|
+
ormbridge never fails silently on something it cannot represent: each case produces a `warning:` on stderr naming the model and field. The main ones:
|
|
312
|
+
|
|
313
|
+
- `settings.AUTH_USER_MODEL` and `get_user_model()` are assumed to be a model named `User`. Models referenced but not found in the input get a stub with an auto-increment id.
|
|
314
|
+
- `GeneratedField` expressions are Python (`F`, `Concat`, ...), so they are carried only into Django output; Prisma and TypeORM get a regular column and a warning. Range fields are `Unsupported(...)` in Prisma and `string` in TypeORM and TypeScript objects (`{ lower, upper, bounds }`), nested arrays are `Json` in Prisma, `size=` on `ArrayField` is not kept, and `FileField` / `ImageField` / `FilePathField` / `GenericIPAddressField(protocol=...)` options are not kept (they come back as `CharField` or a plain IP field). `UniqueConstraint(condition=...)` (partial unique constraints), `GenericForeignKey` / `GenericRelation` and `CheckConstraint` are skipped with a warning.
|
|
315
|
+
- Python is parsed statically, so computed defaults, custom field classes, and fields added dynamically are skipped or approximated.
|
|
316
|
+
- Prisma composite primary keys become `models.CompositePrimaryKey` (Django 5.2+). Composite foreign keys, `Unsupported(...)` types, views, `@@fulltext`, `@@schema`, `@ignore` and client-generated defaults (`cuid()`, `ulid()`, `nanoid()`) have no direct Django, TypeORM, TypeScript or Graphene equivalent; the composite key columns stay as plain fields, a view becomes a table, and each case warns. Scalar lists become `ArrayField` (PostgreSQL only).
|
|
317
|
+
- Not read from Prisma schemas: composite types (`type` blocks, MongoDB), `relationMode`, the client-side `name:` of `@@unique` / `@@id`, partial indexes (`where:`), `@default(..., map:)` constraint names (SQL Server), and sort / length options on `@@id`. Native types are matched to the target provider by name only (the table was built with `prisma validate`); arguments such as a `VarChar` length are passed through unchanged. MongoDB output warns about ids that are not `@map("_id") @db.ObjectId` and about `Decimal` and compound ids, which Prisma rejects there. SQL Server cannot index `NVarChar(Max)` and MySQL needs a prefix length to index `TEXT`; indexes on such columns warn.
|
|
318
|
+
- Prisma's implicit many-to-many join table (`_AToB`) differs from Django's, so data needs migrating.
|
|
319
|
+
- TypeORM: entities are read from decorators only. Not supported, each with a warning naming the entity and column: `EntitySchema`, `@ChildEntity` / `@TableInheritance` (single-table inheritance), `@ViewEntity`, `@Tree*`, `@ObjectIdColumn`, `@VirtualColumn`, array columns, partial and spatial indexes, `@Check` / `@Exclusion`, composite foreign keys, relations inside embedded entities, and column options such as `unsigned`, `collation` and `transformer`. Custom `@JoinTable` names are not preserved. Relations without `onDelete` become `NoAction` and relations are nullable unless `nullable: false`, as in TypeORM. Numeric enums become integers.
|
|
320
|
+
- TypeORM many-to-many relations use TypeORM's join table (composite key), so Django's surrogate `id` column on the join table is not reproduced. SQL Server has no enum column, so enums become `varchar`. Database-side UUID defaults are not available on SQLite. MongoDB entities are not supported.
|
|
321
|
+
- Doctrine: only PHP 8 attributes are read. Docblock annotations (`@ORM\Entity`) are detected and reported with a warning (or an error when nothing else is found), and XML / YAML mappings are not read; both are planned. Not supported, each with a warning naming the entity and property: attribute and association overrides, composite foreign keys (several `#[ORM\JoinColumn]`), `columnDefinition`, `unsigned` / `comment` / `collation` and other column `options`, `insertable: false` / `updatable: false`, partial and spatial indexes, table `schema` and `options`, custom id generators and `#[ORM\SequenceGenerator]`, `array` / `object` / `simple_array` / custom DBAL types, int-backed and non-backed enums, relations inside embeddables, and the `#[ORM\DiscriminatorMap]` values (the discriminator column is kept, the map is not). Nullability follows the attribute (`nullable: true`), never the PHP type: Doctrine does not infer it, so `?string` with a plain `#[ORM\Column]` is `NOT NULL`. Custom join table names, ORM-level `cascade` / `fetch` / `orphanRemoval` and other runtime-only settings are not part of the schema and are not converted; `cascade: ['remove']` and `orphanRemoval` warn. A custom naming strategy configured outside the attributes cannot be seen, so Doctrine's underscore naming is assumed. Classes the input does not contain (a vendor base class or trait, an enum in another package) produce a warning that names the missing class.
|
|
322
|
+
- Laravel: the schema is rebuilt statically, so what Laravel decides at run time is not seen. A conditional or loop inside a migration is applied as if it always ran, and a dynamic table or column name is skipped; both warn and name the table. Raw SQL (`DB::statement`, `DB::unprepared`) is not interpreted, so a view, trigger, partial index or column created that way is missing, with a warning naming the table. Not represented, each with a warning naming the table and column: polymorphic relations (`morphs`, `morphTo`, `morphMany`, `morphToMany`; the `_type` and `_id` columns stay plain columns), derived relations (`hasManyThrough`, `hasOneThrough`, `*OfMany`), relation constraints (`->where(...)`), composite foreign keys, foreign keys to tables no migration creates, tables that migrations alter but never create (for example `users` from a package), `set`, geometry and `vector` columns, `macAddress` (kept as a string), identity columns, spatial indexes, custom pivot table and key names (the join table gets the name other formats derive: `blog_post_tags` survives, `post_tag` does not), pivot timestamps and non-cascading pivot keys, and `parentKey` / `relatedKey` that are not the primary key. Casts to custom cast classes and to int-backed enums are ignored. Mass-assignment and visibility properties (`$fillable`, `$guarded`, `$hidden`) are not part of the schema. Soft deletes have no IR equivalent beyond the nullable `deleted_at` column.
|
|
323
|
+
- Generated names for Django indexes are shortened to Django's 30-character limit.
|
|
324
|
+
|
|
325
|
+
Always review the output and run your own migrations/`prisma validate` before applying it to a real database.
|
|
326
|
+
|
|
327
|
+
### Conversion matrix
|
|
328
|
+
|
|
329
|
+
[docs/CONVERSION_MATRIX.md](./docs/CONVERSION_MATRIX.md) shows, for every pair of readable and writable formats (Django, Prisma, TypeORM), whether the schema survives a round trip and exactly what is lost when it does not (for example enum labels and column lengths, and Prisma's explicit join model for many-to-many fields). It is generated from the test fixtures with `npm run docs:matrix`, and the tests fail if it is out of date.
|
|
330
|
+
|
|
331
|
+
Django models are read with a static [tree-sitter](https://tree-sitter.github.io/) parser, so **no Python, Django install, or database is needed**.
|
|
332
|
+
|
|
333
|
+
[Back to Table of Contents](#table-of-contents)
|
|
334
|
+
|
|
335
|
+
## Programmatic API
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { convertText } from 'ormbridge';
|
|
339
|
+
|
|
340
|
+
const result = await convertText([{ path: 'models.py', text: source }], {
|
|
341
|
+
from: 'django',
|
|
342
|
+
to: 'prisma',
|
|
343
|
+
naming: 'preserve',
|
|
344
|
+
provider: 'postgresql',
|
|
345
|
+
header: true,
|
|
346
|
+
appLabel: 'shop',
|
|
347
|
+
autoField: 'int',
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (!result.ok) {
|
|
351
|
+
console.error(`${result.error.code}: ${result.error.message}`);
|
|
352
|
+
} else {
|
|
353
|
+
console.log(result.value.output);
|
|
354
|
+
console.warn(result.value.warnings);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
All functions return a `Result` (`{ ok: true, value } | { ok: false, error }`) instead of throwing, with a descriptive error code and message. Parsers and emitters are exported too (`parseDjango`, `parsePrisma`, `parseTypeorm`, `parseDoctrine`, `parseLaravel`, `emitPrisma`, `emitDjango`, `emitDoctrine`, `emitLaravel`), all built on a shared intermediate representation, which is how new formats plug in.
|
|
359
|
+
|
|
360
|
+
### Format registry
|
|
361
|
+
|
|
362
|
+
Each format is an adapter registered in `src/formats.ts`: a `name`, its file `extensions`, a `description`, and an optional `parse` and/or `emit`. `convertText` and the CLI look formats up in the registry, so a format that only has `emit` is output-only and one that only has `parse` is input-only. Lookups return a `Result` with a descriptive error for unknown or unsupported formats.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import { getFormat, listFormats, registerFormat } from 'ormbridge';
|
|
366
|
+
|
|
367
|
+
const adapter = getFormat('prisma'); // Result<FormatAdapter>
|
|
368
|
+
const formats = listFormats(); // every registered adapter
|
|
369
|
+
registerFormat({
|
|
370
|
+
name: 'example',
|
|
371
|
+
extensions: ['.example'],
|
|
372
|
+
description: 'An output-only example format',
|
|
373
|
+
emit: (schema, options) => ({ ok: true, value: { text: '', warnings: [] } }),
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Emitter output
|
|
378
|
+
|
|
379
|
+
`emit` returns `Result<FormatEmitOutput>`, which is either `{ text, warnings }` (one file, what every built-in emitter returns) or `{ files, warnings }`, where `files` maps a relative path (forward slashes, no leading slash, no `..`) to the file text. Return the second form when the format needs several files:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
emit: (schema) => ({
|
|
383
|
+
ok: true,
|
|
384
|
+
value: {
|
|
385
|
+
files: { 'src/Entity/Post.php': '<?php ...', 'config/orm.yaml': '...' },
|
|
386
|
+
warnings: [],
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`convertText` then returns `files` next to an empty `output`, and the CLI writes the map under the `-o` directory. A multi-file format cannot print to stdout.
|
|
392
|
+
|
|
393
|
+
[Back to Table of Contents](#table-of-contents)
|
|
394
|
+
|
|
395
|
+
## License
|
|
396
|
+
|
|
397
|
+
MIT
|
|
398
|
+
|
|
399
|
+
## Copyright
|
|
400
|
+
|
|
401
|
+
(c)2026 Richard McQuiston.
|
|
402
|
+
|
|
403
|
+
## Buy Me a Coffee
|
|
404
|
+
|
|
405
|
+
If this app, code, or repository has helped you or someone you know, please consider donating. I appreciate any help to offset the costs of development and/or AI Credits.
|
|
406
|
+
|
|
407
|
+
[**Donate via Stripe**](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800), or scan:
|
|
408
|
+
|
|
409
|
+
[](https://donate.stripe.com/00w5kD3Gj1Xo9v7gVOcs800)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { EXIT_CONVERSION_ERROR, runCli } from './cliRunner.js';
|
|
3
|
+
runCli(process.argv.slice(2), {
|
|
4
|
+
cwd: process.cwd(),
|
|
5
|
+
stdout: (text) => {
|
|
6
|
+
process.stdout.write(text);
|
|
7
|
+
},
|
|
8
|
+
stderr: (text) => {
|
|
9
|
+
process.stderr.write(text);
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
.then((exitCode) => {
|
|
13
|
+
process.exitCode = exitCode;
|
|
14
|
+
})
|
|
15
|
+
.catch((thrown) => {
|
|
16
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
17
|
+
process.stderr.write(`error: ${message}\n`);
|
|
18
|
+
process.exitCode = EXIT_CONVERSION_ERROR;
|
|
19
|
+
});
|
|
20
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE/D,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC5B,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;IAClB,MAAM,EAAE,CAAC,IAAY,EAAQ,EAAE;QAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,EAAE,CAAC,IAAY,EAAQ,EAAE;QAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;CACF,CAAC;KACC,IAAI,CAAC,CAAC,QAAgB,EAAE,EAAE;IACzB,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAC9B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,MAAe,EAAE,EAAE;IACzB,MAAM,OAAO,GACX,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;IAC5C,OAAO,CAAC,QAAQ,GAAG,qBAAqB,CAAC;AAC3C,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Exit codes of the `ormbridge` command. */
|
|
2
|
+
export declare const EXIT_OK: number;
|
|
3
|
+
/** A conversion failed: unreadable input, parse error, unwritable output. */
|
|
4
|
+
export declare const EXIT_CONVERSION_ERROR: number;
|
|
5
|
+
/** The command line or the config file is invalid. */
|
|
6
|
+
export declare const EXIT_USAGE_ERROR: number;
|
|
7
|
+
/** `--check` found an output file that is stale or missing. */
|
|
8
|
+
export declare const EXIT_CHECK_FAILED: number;
|
|
9
|
+
/** Where the CLI reads its working directory from and writes its output to. */
|
|
10
|
+
export interface CliEnvironment {
|
|
11
|
+
cwd: string;
|
|
12
|
+
stdout: (text: string) => void;
|
|
13
|
+
stderr: (text: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs the CLI with the given arguments (without the node and script
|
|
17
|
+
* entries) and returns the exit code. Nothing is written to the real
|
|
18
|
+
* process streams, so tests can call this in-process.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runCli(argv: string[], env: CliEnvironment): Promise<number>;
|