Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"watch": "tsc -watch -p ./",
"pretest": "npm run compile",
"lint": "eslint src --ext ts",
"test": "node --test tests/smoke.test.js"
"test": "node --test tests/*.test.js"
},
"devDependencies": {
"@types/node": "^20.0.0",
Expand Down
3 changes: 2 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import { execFile, execFileSync } from 'child_process';
import { SCHEMA_FORMATS } from './formats';

/**
* Get the schemaforge CLI path from settings or default to 'schemaforge'.
Expand Down Expand Up @@ -99,6 +100,6 @@ export async function getAvailableFormats(): Promise<string[]> {
return JSON.parse(result);
} catch {
// Fallback
return ['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala'];
return SCHEMA_FORMATS;
}
}
5 changes: 3 additions & 2 deletions src/commands/convert.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import { execSchemaForge } from '../cli';
import { getOutputChannel } from '../output';
import { SCHEMA_FORMATS, normalizeFormat } from '../formats';

export class ConvertCommand {
static async run(uri?: vscode.Uri) {
Expand All @@ -23,10 +24,10 @@ export class ConvertCommand {

// Detect format first
const detectResult = await execSchemaForge(['detect', sourcePath]);
const detectedFormat = detectResult.trim();
const detectedFormat = normalizeFormat(detectResult) ?? '';

// Let user pick target format
const formats = ['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala'];
const formats = SCHEMA_FORMATS;
const target = await vscode.window.showQuickPick(
formats.filter(f => f !== detectedFormat),
{ placeHolder: `Source: ${detectedFormat}. Pick target format:`, canPickMany: false }
Expand Down
50 changes: 50 additions & 0 deletions src/formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Canonical list of schema formats SchemaForge converts between.
*
* Previously this list was inlined in three places (convert.ts,
* previewPanel.ts, and cli.ts's getAvailableFormats fallback). Keeping a
* single source of truth prevents the list from silently drifting between
* call sites — a recurring failure class in this extension (see PR #9, where
* the quickConvert default fallback had already diverged from package.json).
*/
export const SCHEMA_FORMATS: string[] = [
'sql',
'prisma',
'drizzle',
'typeorm',
'django',
'sqlalchemy',
'alembic',
'json_schema',
'graphql',
'ef',
'scala',
];

const FORMAT_SET = new Set(SCHEMA_FORMATS);

/**
* Normalize a format identifier produced by the schemaforge CLI (e.g. from
* `detect`) into the canonical token the `convert --from/--to` flags expect.
*
* The CLI sometimes emits mixed-case or padded labels (e.g. "SQL", " Prisma ").
* Passing those verbatim to `convert` fails with an opaque CLI error while the
* command appears to "do nothing" — the classic silent-failure trap. Here we
* trim + lowercase and, when the value matches a known format, return its
* canonical spelling so the conversion actually runs. Unknown values are passed
* through unchanged so the CLI still produces a clear, actionable error.
*/
export function normalizeFormat(fmt: string | undefined | null): string | undefined {
if (fmt === undefined || fmt === null) {
return undefined;
}
const trimmed = fmt.trim();
if (trimmed.length === 0) {
return undefined;
}
const lower = trimmed.toLowerCase();
if (FORMAT_SET.has(lower)) {
return lower;
}
return trimmed;
}
5 changes: 3 additions & 2 deletions src/panels/previewPanel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import { execSchemaForge } from '../cli';
import { SCHEMA_FORMATS, normalizeFormat } from '../formats';

/**
* Generate a random nonce so the webview's Content-Security-Policy can
Expand Down Expand Up @@ -87,10 +88,10 @@ export class SchemaPreviewPanel {
try {
// Detect format
const detectResult = await execSchemaForge(['detect', this.currentFile]);
const sourceFormat = detectResult.trim();
const sourceFormat = normalizeFormat(detectResult) ?? '';

// Get all formats
const allFormats = ['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala'];
const allFormats = SCHEMA_FORMATS;
const targetFormats = allFormats.filter(f => f !== sourceFormat);

// Convert to all other formats (limit to 5 most relevant to keep it fast)
Expand Down
49 changes: 49 additions & 0 deletions tests/formats.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const test = require("node:test");
const assert = require("node:assert");
const path = require("node:path");
const fs = require("node:fs");

// Load the compiled module (the `pretest` script compiles src -> out).
const formats = require(path.join(__dirname, "..", "out", "formats.js"));

test("normalizeFormat lowercases and trims detected labels", () => {
assert.strictEqual(formats.normalizeFormat("SQL"), "sql");
assert.strictEqual(formats.normalizeFormat(" Prisma "), "prisma");
assert.strictEqual(formats.normalizeFormat("JSON_SCHEMA"), "json_schema");
});

test("normalizeFormat maps to the canonical token for known formats", () => {
for (const f of formats.SCHEMA_FORMATS) {
assert.strictEqual(formats.normalizeFormat(f.toUpperCase()), f);
}
});

test("normalizeFormat passes unknown formats through (so the CLI errors clearly)", () => {
assert.strictEqual(formats.normalizeFormat("mongodb"), "mongodb");
});

test("normalizeFormat handles empty / undefined / null input", () => {
assert.strictEqual(formats.normalizeFormat(""), undefined);
assert.strictEqual(formats.normalizeFormat(" "), undefined);
assert.strictEqual(formats.normalizeFormat(undefined), undefined);
assert.strictEqual(formats.normalizeFormat(null), undefined);
});

test("SCHEMA_FORMATS matches the package.json defaultTargetFormat enum", () => {
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"));
const enumVals = pkg.contributes.configuration.properties["schemaforge.defaultTargetFormat"].enum;
assert.deepStrictEqual(formats.SCHEMA_FORMATS.slice().sort(), enumVals.slice().sort());
});

test("source call sites import SCHEMA_FORMATS instead of inlining the list", () => {
const root = path.join(__dirname, "..");
const files = ["src/commands/convert.ts", "src/panels/previewPanel.ts", "src/cli.ts"];
for (const rel of files) {
const src = fs.readFileSync(path.join(root, rel), "utf-8");
assert.doesNotMatch(
src,
/\['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala'\]/,
`${rel} should import SCHEMA_FORMATS instead of inlining the list`
);
}
});
Loading