Skip to content

Commit c441848

Browse files
improve: harden packaging metadata with license file, expanded keywords, and project URLs (#38)
* improve: harden packaging metadata with license file, expanded keywords, and project URLs * fix: remove broken release-audit workflow referencing non-existent repo * fix: add CLI integration tests for detect, formats, and positional convert arg by reviewer-B * Confine MCP check tool to a root; make docs accurate - mcp: confine 'check' directory reads to SCHEMAFORGE_MCP_ROOT (default cwd) - docs: 'zero-loss' -> high-fidelity with FK/relationship limitation noted; 110 -> 100 conversion directions; CHANGELOG detect_format -> detect * fix: fix ruff lint errors in mcp_server.py - move imports to top of file and sort imports
1 parent 53490e0 commit c441848

6 files changed

Lines changed: 242 additions & 30 deletions

File tree

‎CHANGELOG.md‎

144 Bytes
Binary file not shown.

‎README.md‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SchemaForge
22

3-
> **Bidirectional ORM schema converter** — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic migrations, JSON Schema, GraphQL SDL, EF Core (C#), and Scala case classes. **11 formats, 110 direction pairs.**
3+
> **Bidirectional ORM schema converter** — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic migrations, JSON Schema, GraphQL SDL, EF Core (C#), and Scala case classes. **11 formats, 100 conversion directions.**
44
55
[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/schemaforge?style=social)](https://github.com/Coding-Dev-Tools/schemaforge/stargazers)
66
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/Coding-Dev-Tools/schemaforge)
@@ -55,7 +55,7 @@ Requires Python 3.10+.
5555

5656
### `schemaforge convert`
5757

58-
Convert a schema from one format to another. All 11 formats support conversion to and from every other format (110 direction pairs).
58+
Convert a schema from one format to another. Every format converts to and from every other format, except Alembic which is generator-only (a target, not a source) — 100 direction pairs.
5959

6060
```bash
6161
# Format-specific examples
@@ -115,6 +115,11 @@ Detects added, removed, and modified tables, columns, indexes, and constraints.
115115

116116
**Alembic** is generator-only: you can create migration scripts from any format, but parsing existing migrations back to IR is not yet supported.
117117

118+
### Limitations
119+
120+
- **Foreign keys & relationships** — the shared IR does not yet model foreign-key constraints or ORM relations, so `FOREIGN KEY` / `REFERENCES` clauses, Prisma/TypeORM relation fields, and Django `ForeignKey` fields are dropped during conversion rather than roundtripped. Tables, columns, types, defaults, indexes, unique constraints, and enums are preserved. FK support is on the roadmap.
121+
- **Alembic is generator-only** (see above) — you can generate migrations from any format but not parse them back.
122+
118123
### Format Identifiers for `--from` / `--to`
119124

120125
| CLI identifier | Format |
@@ -135,8 +140,8 @@ Detects added, removed, and modified tables, columns, indexes, and constraints.
135140

136141
SchemaForge uses a **shared Internal Representation (IR)** — all formats convert to and from this common schema definition. This architecture guarantees:
137142

138-
- **Zero-loss roundtripping**: `sql → prisma → sql` produces the same schema you started with
139-
- **Bidirectional conversion**: every supported format can convert to every other format
143+
- **High-fidelity roundtripping**: `sql → prisma → sql` reproduces tables, columns, types, defaults, indexes, unique constraints, and enums. Foreign-key/relationship constraints are not yet modeled in the IR and are dropped (see [Limitations](#limitations)).
144+
- **Bidirectional conversion**: every format can convert to every other format, except Alembic, which is generator-only (a target, not a source)
140145
- **Extensibility**: adding a new format requires only a parser and a generator — no pairwise converters
141146

142147
```
@@ -238,8 +243,8 @@ Each fixture demonstrates the same blog schema so you can compare ORM syntax sid
238243

239244
## Features
240245

241-
- **Bidirectional conversion** — all 11 formats convert to and from every other format
242-
- **Zero-loss roundtripping** — `sql → prisma → sql` reproduces the original schema exactly
246+
- **Bidirectional conversion** — every format converts to and from every other format (Alembic is generator-only: a target, not a source)
247+
- **High-fidelity roundtripping** — `sql → prisma → sql` reproduces tables, columns, types, defaults, indexes, and enums (foreign keys are not yet preserved — see [Limitations](#limitations))
243248
- **Custom type mappings** — YAML/JSON config files to override any type mapping with template variables
244249
- **VS Code extension** — live preview, schema diff, and one-click conversion from VS Code
245250
- **Alembic migration generation** — create database migration scripts from any schema format
@@ -253,7 +258,7 @@ Each fixture demonstrates the same blog schema so you can compare ORM syntax sid
253258
- **Function default preservation** — `CURRENT_TIMESTAMP`, `NOW()`, `gen_random_uuid()` survive roundtrips
254259
- **MySQL support** — ENGINE=InnoDB, AUTO_INCREMENT, DEFAULT CHARSET, COMMENT table options
255260
- **Inline ENUM** — `ENUM('small', 'medium', 'large')` column types parsed and roundtripped
256-
- **Relation preservation** — indexes, unique constraints maintained across all conversions
261+
- **Index & constraint preservation** — indexes and unique constraints maintained across all conversions (foreign-key/relationship constraints are not yet modeled — see [Limitations](#limitations))
257262
- **Custom type handling** — dialect-specific types (JSONB, etc.) pass through via CUSTOM type
258263

259264
## MCP Server
@@ -442,10 +447,4 @@ MIT — see [LICENSE](LICENSE)
442447

443448
---
444449

445-
<sub>Part of [Revenue Holdings](https://coding-dev-tools.github.io/devforge/) — a suite of 11 developer CLI tools built by autonomous AI agents. Also check out the [SchemaForge VS Code extension](https://github.com/Coding-Dev-Tools/vscode-schemaforge), [ConfigDrift](https://github.com/Coding-Dev-Tools/configdrift) (config drift detection), [DataMorph](https://github.com/Coding-Dev-Tools/datamorph) (data format conversion), [DeadCode](https://github.com/Coding-Dev-Tools/deadcode) (dead code cleanup), [DeployDiff](https://github.com/Coding-Dev-Tools/deploydiff) (infrastructure diffs), [Envault](https://github.com/Coding-Dev-Tools/envault) (env sync), [APIAuth](https://github.com/Coding-Dev-Tools/apiauth) (API key management), [APIGhost](https://github.com/Coding-Dev-Tools/apighost) (mock API server), [json2sql](https://github.com/Coding-Dev-Tools/json2sql) (JSON → SQL), [API Contract Guardian](https://github.com/Coding-Dev-Tools/api-contract-guardian) (breaking change detection), and [click-to-mcp](https://github.com/Coding-Dev-Tools/click-to-mcp) (CLI → MCP server).</sub>
446-
447-
## Test
448-
449-
```bash
450-
npm test # runs: node --test tests/
451-
```
450+
<sub>Part of [Revenue Holdings](https://coding-dev-tools.github.io/devforge/) — a s

‎pyproject.toml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ version = "1.7.0"
88
description = "Bidirectional ORM schema converter — convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy, Alembic, JSON Schema, GraphQL SDL, EF Core (C#), and Scala case classes with zero-loss roundtripping"
99
readme = "README.md"
1010
requires-python = ">=3.10"
11-
license = "MIT"
11+
license = {file = "LICENSE"}
1212
authors = [{name = "Revenue Holdings"}]
13-
keywords = ["schema", "orm", "prisma", "drizzle", "typeorm", "django", "sql", "converter", "migration"]
13+
keywords = ["schema", "orm", "prisma", "drizzle", "typeorm", "django", "sql", "converter", "migration", "alembic", "graphql", "json-schema", "ef-core", "scala", "code-generation"]
1414
classifiers = [
1515
"Development Status :: 4 - Beta",
1616
"Intended Audience :: Developers",
1717
"Topic :: Database",
1818
"Topic :: Software Development :: Code Generators",
1919
"Operating System :: OS Independent",
20+
"License :: OSI Approved :: MIT License",
2021
"Programming Language :: Python :: 3",
2122
"Programming Language :: Python :: 3.10",
2223
"Programming Language :: Python :: 3.11",
@@ -48,13 +49,15 @@ schemaforge = "schemaforge.cli:main"
4849
Homepage = "https://github.com/Coding-Dev-Tools/schemaforge"
4950
Repository = "https://github.com/Coding-Dev-Tools/schemaforge"
5051
"Issue Tracker" = "https://github.com/Coding-Dev-Tools/schemaforge/issues"
52+
Documentation = "https://github.com/Coding-Dev-Tools/schemaforge#readme"
53+
Changelog = "https://github.com/Coding-Dev-Tools/schemaforge/blob/main/CHANGELOG.md"
5154

5255
[tool.setuptools.packages.find]
5356
where = ["src"]
5457

55-
5658
[tool.setuptools.package-data]
5759
"*" = ["py.typed"]
60+
5861
[tool.pytest.ini_options]
5962
testpaths = ["tests"]
6063

‎src/schemaforge/cli.py‎

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import click
6+
import json
67
import sys
78
from pathlib import Path
89

@@ -35,28 +36,31 @@ def main() -> None:
3536
"""SchemaForge — bidirectional ORM schema converter.
3637
3738
Convert between SQL DDL, Prisma, Drizzle, TypeORM, Django, SQLAlchemy models,
38-
Alembic migrations, JSON Schema, and GraphQL SDL with zero-loss roundtripping.
39+
Alembic migrations, JSON Schema, and GraphQL SDL with high-fidelity
40+
roundtripping (foreign-key/relationship constraints are not yet preserved).
3941
"""
4042

4143

4244
@main.command()
45+
@click.argument(
46+
"input_arg",
47+
required=False,
48+
type=click.Path(exists=True, readable=True),
49+
)
4350
@click.option(
4451
"--from",
4552
"from_fmt",
4653
required=True,
4754
type=click.Choice(_FORMATS),
4855
help="Source format",
4956
)
50-
@click.option(
51-
"--to", "to_fmt", required=True, type=click.Choice(_FORMATS), help="Target format"
52-
)
57+
@click.option("--to", "to_fmt", required=True, type=click.Choice(_FORMATS), help="Target format")
5358
@click.option(
5459
"--input",
5560
"-i",
56-
"input_path",
57-
required=True,
61+
"input_opt",
5862
type=click.Path(exists=True, readable=True),
59-
help="Input file path",
63+
help="Input file path (alternative to the positional INPUT_ARG)",
6064
)
6165
@click.option(
6266
"--output",
@@ -72,13 +76,27 @@ def main() -> None:
7276
help="Custom type mapping config file (.yaml or .json)",
7377
)
7478
def convert(
79+
input_arg: str | None,
7580
from_fmt: str,
7681
to_fmt: str,
77-
input_path: str,
82+
input_opt: str | None,
7883
output_path: str | None,
7984
type_map_path: str | None,
8085
) -> None:
81-
"""Convert schema between formats."""
86+
"""Convert schema between formats.
87+
88+
The input file may be given either as a positional argument
89+
(``schemaforge convert schema.sql --from sql --to prisma``) or via
90+
``--input``/``-i`` — the two are interchangeable.
91+
"""
92+
input_path = input_arg or input_opt
93+
if not input_path:
94+
click.echo(
95+
"Error: no input file given. Pass a path argument or use --input/-i.",
96+
err=True,
97+
)
98+
sys.exit(1)
99+
82100
# Load custom type mapping if specified
83101
type_config: TypeConfig | None = None
84102
if type_map_path:
@@ -157,9 +175,7 @@ def check(directory: str, canonical: str, type_map_path: str | None) -> None:
157175
consistency across format representations.
158176
"""
159177
try:
160-
result = check_directory(
161-
directory, canonical=canonical, type_map_path=type_map_path
162-
)
178+
result = check_directory(directory, canonical=canonical, type_map_path=type_map_path)
163179
click.echo(result)
164180
if "FAIL" in result and "PASS" not in result:
165181
sys.exit(1)
@@ -168,6 +184,43 @@ def check(directory: str, canonical: str, type_map_path: str | None) -> None:
168184
sys.exit(1)
169185

170186

187+
@main.command()
188+
@click.argument("input_path", type=click.Path(exists=True, readable=True))
189+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed detection info")
190+
def detect(input_path: str, verbose: bool) -> None:
191+
"""Detect the schema format of a file from its extension.
192+
193+
Prints the bare format identifier (e.g. ``prisma``) on success, or
194+
``unknown`` if the extension is not recognized. The plain output is meant
195+
to be consumed directly (the VS Code extension reads it as the source
196+
format for a follow-up convert).
197+
"""
198+
fmt = detect_format(input_path)
199+
if verbose:
200+
ext = Path(input_path).suffix.lower() or "(none)"
201+
click.echo(f"file: {input_path}")
202+
click.echo(f"extension: {ext}")
203+
click.echo(f"format: {fmt if fmt else 'unknown'}")
204+
click.echo("method: file extension")
205+
else:
206+
click.echo(fmt if fmt else "unknown")
207+
208+
209+
@main.command()
210+
@click.option("--json", "as_json", is_flag=True, help="Output the format list as a JSON array")
211+
def formats(as_json: bool) -> None:
212+
"""List all supported schema formats.
213+
214+
With ``--json`` prints a JSON array of format identifiers (consumed by the
215+
VS Code extension); otherwise prints one format identifier per line.
216+
"""
217+
if as_json:
218+
click.echo(json.dumps(_FORMATS))
219+
else:
220+
for fmt in _FORMATS:
221+
click.echo(fmt)
222+
223+
171224
# Register the MCP server subcommand
172225
main.add_command(mcp_command)
173226

‎src/schemaforge/mcp_server.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import click
11+
import os
1112
from pathlib import Path
1213
from typing import Any
1314

@@ -23,6 +24,26 @@
2324
FastMCP = None # type: ignore
2425

2526

27+
def _confined_directory(directory: str) -> Path:
28+
"""Resolve *directory* and confirm it stays within the allowed root.
29+
30+
The ``check`` tool iterates and reads files under the given directory. To
31+
keep an AI agent (or, in SSE mode, a remote caller) from reading arbitrary
32+
locations on the host, requests are confined to a root — the
33+
``SCHEMAFORGE_MCP_ROOT`` environment variable if set, otherwise the current
34+
working directory the server was launched in. Escaping the root raises
35+
``PermissionError``.
36+
"""
37+
root = Path(os.environ.get("SCHEMAFORGE_MCP_ROOT", Path.cwd())).resolve()
38+
target = Path(directory).resolve()
39+
if target != root and not target.is_relative_to(root):
40+
raise PermissionError(
41+
f"Directory '{directory}' is outside the allowed root '{root}'. "
42+
f"Set SCHEMAFORGE_MCP_ROOT to permit a different base directory."
43+
)
44+
return target
45+
46+
2647
# All supported formats
2748
_FORMATS = [
2849
"sql",
@@ -156,10 +177,13 @@ def check_tool(
156177
type_map_path: Optional path to a YAML/JSON type mapping config file.
157178
"""
158179
try:
180+
safe_dir = _confined_directory(directory)
159181
result = check_directory(
160-
directory, canonical=canonical, type_map_path=type_map_path
182+
str(safe_dir), canonical=canonical, type_map_path=type_map_path
161183
)
162184
return result
185+
except PermissionError as e:
186+
return f"Error: {e}"
163187
except NotADirectoryError as e:
164188
return f"Error: {e}"
165189
except Exception as e:

0 commit comments

Comments
 (0)