@sqg/sqg 0.4.0 → 0.5.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/dist/sqg.mjs +343 -240
- package/package.json +1 -1
package/dist/sqg.mjs
CHANGED
|
@@ -21,45 +21,128 @@ import typescriptPlugin from "prettier/parser-typescript";
|
|
|
21
21
|
import estree from "prettier/plugins/estree";
|
|
22
22
|
|
|
23
23
|
//#region src/constants.ts
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"sqlite",
|
|
31
|
-
"duckdb",
|
|
32
|
-
"postgres"
|
|
33
|
-
];
|
|
34
|
-
/** Supported code generators with their descriptions */
|
|
35
|
-
const SUPPORTED_GENERATORS = {
|
|
36
|
-
"typescript/better-sqlite3": {
|
|
24
|
+
/** All supported generators with their full specification */
|
|
25
|
+
const GENERATORS = {
|
|
26
|
+
"typescript/sqlite/better-sqlite3": {
|
|
27
|
+
language: "typescript",
|
|
28
|
+
engine: "sqlite",
|
|
29
|
+
driver: "better-sqlite3",
|
|
37
30
|
description: "TypeScript with better-sqlite3 driver",
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
extension: ".ts",
|
|
32
|
+
template: "better-sqlite3.hbs"
|
|
40
33
|
},
|
|
41
|
-
"typescript/duckdb": {
|
|
34
|
+
"typescript/duckdb/node-api": {
|
|
35
|
+
language: "typescript",
|
|
36
|
+
engine: "duckdb",
|
|
37
|
+
driver: "node-api",
|
|
42
38
|
description: "TypeScript with @duckdb/node-api driver",
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
extension: ".ts",
|
|
40
|
+
template: "typescript-duckdb.hbs"
|
|
41
|
+
},
|
|
42
|
+
"java/sqlite/jdbc": {
|
|
43
|
+
language: "java",
|
|
44
|
+
engine: "sqlite",
|
|
45
|
+
driver: "jdbc",
|
|
46
|
+
description: "Java with JDBC for SQLite",
|
|
47
|
+
extension: ".java",
|
|
48
|
+
template: "java-jdbc.hbs"
|
|
45
49
|
},
|
|
46
|
-
"java/jdbc": {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
extension: ".java"
|
|
50
|
+
"java/duckdb/jdbc": {
|
|
51
|
+
language: "java",
|
|
52
|
+
engine: "duckdb",
|
|
53
|
+
driver: "jdbc",
|
|
54
|
+
description: "Java with JDBC for DuckDB",
|
|
55
|
+
extension: ".java",
|
|
56
|
+
template: "java-jdbc.hbs"
|
|
54
57
|
},
|
|
55
|
-
"java/duckdb
|
|
58
|
+
"java/duckdb/arrow": {
|
|
59
|
+
language: "java",
|
|
60
|
+
engine: "duckdb",
|
|
61
|
+
driver: "arrow",
|
|
56
62
|
description: "Java with DuckDB Arrow API",
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
extension: ".java",
|
|
64
|
+
template: "java-duckdb-arrow.hbs"
|
|
65
|
+
},
|
|
66
|
+
"java/postgres/jdbc": {
|
|
67
|
+
language: "java",
|
|
68
|
+
engine: "postgres",
|
|
69
|
+
driver: "jdbc",
|
|
70
|
+
description: "Java with JDBC for PostgreSQL",
|
|
71
|
+
extension: ".java",
|
|
72
|
+
template: "java-jdbc.hbs"
|
|
59
73
|
}
|
|
60
74
|
};
|
|
61
|
-
/**
|
|
62
|
-
const
|
|
75
|
+
/** Default drivers for language/engine combinations */
|
|
76
|
+
const DEFAULT_DRIVERS = {
|
|
77
|
+
"typescript/sqlite": "better-sqlite3",
|
|
78
|
+
"typescript/duckdb": "node-api",
|
|
79
|
+
"java/sqlite": "jdbc",
|
|
80
|
+
"java/duckdb": "jdbc",
|
|
81
|
+
"java/postgres": "jdbc"
|
|
82
|
+
};
|
|
83
|
+
/** List of all full generator names */
|
|
84
|
+
const GENERATOR_NAMES = Object.keys(GENERATORS);
|
|
85
|
+
/** List of short generator names (language/engine) */
|
|
86
|
+
const SHORT_GENERATOR_NAMES = Object.keys(DEFAULT_DRIVERS);
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a generator string to its full form.
|
|
89
|
+
* Accepts both short (language/engine) and full (language/engine/driver) formats.
|
|
90
|
+
*/
|
|
91
|
+
function resolveGenerator(generator) {
|
|
92
|
+
if (generator in GENERATORS) return generator;
|
|
93
|
+
if (generator in DEFAULT_DRIVERS) return `${generator}/${DEFAULT_DRIVERS[generator]}`;
|
|
94
|
+
return generator;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse a generator string and return its info.
|
|
98
|
+
* Throws if the generator is invalid.
|
|
99
|
+
*/
|
|
100
|
+
function parseGenerator(generator) {
|
|
101
|
+
const info = GENERATORS[resolveGenerator(generator)];
|
|
102
|
+
if (!info) throw new Error(`Invalid generator: ${generator}`);
|
|
103
|
+
return info;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a generator string is valid (either short or full form).
|
|
107
|
+
*/
|
|
108
|
+
function isValidGenerator(generator) {
|
|
109
|
+
return resolveGenerator(generator) in GENERATORS;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the database engine for a generator.
|
|
113
|
+
*/
|
|
114
|
+
function getGeneratorEngine(generator) {
|
|
115
|
+
return parseGenerator(generator).engine;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Find similar generator names for typo suggestions.
|
|
119
|
+
*/
|
|
120
|
+
function findSimilarGenerators(input) {
|
|
121
|
+
const normalized = input.toLowerCase();
|
|
122
|
+
return [...GENERATOR_NAMES, ...SHORT_GENERATOR_NAMES].filter((name) => {
|
|
123
|
+
const nameLower = name.toLowerCase();
|
|
124
|
+
if (nameLower.includes(normalized) || normalized.includes(nameLower)) return true;
|
|
125
|
+
return [
|
|
126
|
+
normalized.replace(/\//g, "-"),
|
|
127
|
+
normalized.replace(/-/g, "/"),
|
|
128
|
+
normalized.replace(/_/g, "/"),
|
|
129
|
+
normalized.replace(/_/g, "-")
|
|
130
|
+
].some((v) => nameLower.includes(v) || v.includes(nameLower));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Format generators for CLI help output.
|
|
135
|
+
*/
|
|
136
|
+
function formatGeneratorsHelp() {
|
|
137
|
+
const lines = [];
|
|
138
|
+
for (const shortName of SHORT_GENERATOR_NAMES) {
|
|
139
|
+
const fullName = `${shortName}/${DEFAULT_DRIVERS[shortName]}`;
|
|
140
|
+
const info = GENERATORS[fullName];
|
|
141
|
+
lines.push(` ${shortName.padEnd(24)} ${info.description} (default)`);
|
|
142
|
+
for (const [generatorName, generatorInfo] of Object.entries(GENERATORS)) if (generatorName.startsWith(`${shortName}/`) && generatorName !== fullName) lines.push(` ${generatorName.padEnd(24)} ${generatorInfo.description}`);
|
|
143
|
+
}
|
|
144
|
+
return lines.join("\n");
|
|
145
|
+
}
|
|
63
146
|
/** SQL annotation syntax reference */
|
|
64
147
|
const SQL_SYNTAX_REFERENCE = `
|
|
65
148
|
SQL Annotation Syntax:
|
|
@@ -88,34 +171,6 @@ Example:
|
|
|
88
171
|
|
|
89
172
|
-- TABLE users :appender
|
|
90
173
|
`.trim();
|
|
91
|
-
/**
|
|
92
|
-
* Find similar generator names for typo suggestions
|
|
93
|
-
*/
|
|
94
|
-
function findSimilarGenerators(input) {
|
|
95
|
-
const normalized = input.toLowerCase();
|
|
96
|
-
return GENERATOR_NAMES.filter((name) => {
|
|
97
|
-
const nameLower = name.toLowerCase();
|
|
98
|
-
if (nameLower.includes(normalized) || normalized.includes(nameLower)) return true;
|
|
99
|
-
return [
|
|
100
|
-
normalized.replace("/", "-"),
|
|
101
|
-
normalized.replace("-", "/"),
|
|
102
|
-
normalized.replace("_", "/"),
|
|
103
|
-
normalized.replace("_", "-")
|
|
104
|
-
].some((v) => nameLower.includes(v) || v.includes(nameLower));
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Format generators for CLI help output
|
|
109
|
-
*/
|
|
110
|
-
function formatGeneratorsHelp() {
|
|
111
|
-
return Object.entries(SUPPORTED_GENERATORS).map(([name, info]) => ` ${name.padEnd(28)} ${info.description} (${info.compatibleEngines.join(", ")})`).join("\n");
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Format engines for CLI help output
|
|
115
|
-
*/
|
|
116
|
-
function formatEnginesHelp() {
|
|
117
|
-
return DB_ENGINES.map((e) => ` ${e}`).join("\n");
|
|
118
|
-
}
|
|
119
174
|
|
|
120
175
|
//#endregion
|
|
121
176
|
//#region src/errors.ts
|
|
@@ -174,41 +229,20 @@ var ConfigError = class extends SqgError {
|
|
|
174
229
|
* Error for invalid generator names
|
|
175
230
|
*/
|
|
176
231
|
var InvalidGeneratorError = class extends SqgError {
|
|
177
|
-
constructor(generatorName, validGenerators, suggestion) {
|
|
232
|
+
constructor(generatorName, validGenerators$1, suggestion) {
|
|
178
233
|
const similarMsg = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
179
|
-
super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators.join(", ")}`, { generator: generatorName });
|
|
234
|
+
super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators$1.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators$1.join(", ")}`, { generator: generatorName });
|
|
180
235
|
this.name = "InvalidGeneratorError";
|
|
181
236
|
}
|
|
182
237
|
};
|
|
183
238
|
/**
|
|
184
|
-
* Error for invalid engine names
|
|
185
|
-
*/
|
|
186
|
-
var InvalidEngineError = class extends SqgError {
|
|
187
|
-
constructor(engineName, validEngines) {
|
|
188
|
-
super(`Invalid engine '${engineName}'. Valid engines: ${validEngines.join(", ")}`, "INVALID_ENGINE", `Choose from: ${validEngines.join(", ")}`, { engine: engineName });
|
|
189
|
-
this.name = "InvalidEngineError";
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* Error for generator/engine compatibility
|
|
194
|
-
*/
|
|
195
|
-
var GeneratorEngineMismatchError = class extends SqgError {
|
|
196
|
-
constructor(generator, engine, compatibleEngines) {
|
|
197
|
-
super(`Generator '${generator}' is not compatible with engine '${engine}'`, "GENERATOR_ENGINE_MISMATCH", `Generator '${generator}' works with: ${compatibleEngines.join(", ")}`, {
|
|
198
|
-
generator,
|
|
199
|
-
engine
|
|
200
|
-
});
|
|
201
|
-
this.name = "GeneratorEngineMismatchError";
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
/**
|
|
205
239
|
* Error for database initialization/connection issues
|
|
206
240
|
*/
|
|
207
241
|
var DatabaseError = class extends SqgError {
|
|
208
242
|
constructor(message, engine, suggestion, context) {
|
|
209
243
|
super(message, "DATABASE_ERROR", suggestion, {
|
|
210
|
-
|
|
211
|
-
|
|
244
|
+
...context,
|
|
245
|
+
engine
|
|
212
246
|
});
|
|
213
247
|
this.name = "DatabaseError";
|
|
214
248
|
}
|
|
@@ -1457,7 +1491,7 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
|
|
|
1457
1491
|
const name = `${gen.name}-jdbc`;
|
|
1458
1492
|
writeGeneratedFile(projectDir, {
|
|
1459
1493
|
name,
|
|
1460
|
-
generator: "java/jdbc",
|
|
1494
|
+
generator: "java/duckdb/jdbc",
|
|
1461
1495
|
output: gen.output,
|
|
1462
1496
|
config: gen.config
|
|
1463
1497
|
}, this.javaGenerator, name, q, tables, "duckdb");
|
|
@@ -1754,16 +1788,25 @@ var TsDuckDBGenerator = class extends TsGenerator {
|
|
|
1754
1788
|
|
|
1755
1789
|
//#endregion
|
|
1756
1790
|
//#region src/generators/index.ts
|
|
1791
|
+
/**
|
|
1792
|
+
* Get a generator instance for the given generator.
|
|
1793
|
+
* Accepts both short (language/engine) and full (language/engine/driver) formats.
|
|
1794
|
+
*/
|
|
1757
1795
|
function getGenerator(generator) {
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1796
|
+
const fullGenerator = resolveGenerator(generator);
|
|
1797
|
+
try {
|
|
1798
|
+
const info = parseGenerator(generator);
|
|
1799
|
+
const key = `${info.language}/${info.driver}`;
|
|
1800
|
+
switch (key) {
|
|
1801
|
+
case "typescript/better-sqlite3": return new TsGenerator(`templates/${info.template}`);
|
|
1802
|
+
case "typescript/node-api": return new TsDuckDBGenerator(`templates/${info.template}`);
|
|
1803
|
+
case "java/jdbc": return new JavaGenerator(`templates/${info.template}`);
|
|
1804
|
+
case "java/arrow": return new JavaDuckDBArrowGenerator(`templates/${info.template}`);
|
|
1805
|
+
default: throw new Error(`No generator class for ${key}`);
|
|
1766
1806
|
}
|
|
1807
|
+
} catch {
|
|
1808
|
+
const similar = findSimilarGenerators(generator);
|
|
1809
|
+
throw new InvalidGeneratorError(fullGenerator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
|
|
1767
1810
|
}
|
|
1768
1811
|
}
|
|
1769
1812
|
|
|
@@ -1932,6 +1975,8 @@ function generateSourceFile(name, queries, tables, templatePath, generator, engi
|
|
|
1932
1975
|
allowProtoMethodsByDefault: true
|
|
1933
1976
|
});
|
|
1934
1977
|
}
|
|
1978
|
+
/** All valid generator strings for schema validation */
|
|
1979
|
+
const validGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
|
|
1935
1980
|
/**
|
|
1936
1981
|
* Project configuration schema with descriptions for validation messages
|
|
1937
1982
|
*/
|
|
@@ -1939,10 +1984,9 @@ const ProjectSchema = z.object({
|
|
|
1939
1984
|
version: z.number().describe("Configuration version (currently 1)"),
|
|
1940
1985
|
name: z.string().min(1, "Project name is required").describe("Project name used for generated class names"),
|
|
1941
1986
|
sql: z.array(z.object({
|
|
1942
|
-
engine: z.enum(DB_ENGINES).describe(`Database engine: ${DB_ENGINES.join(", ")}`),
|
|
1943
1987
|
files: z.array(z.string().min(1)).min(1, "At least one SQL file is required").describe("SQL files to process"),
|
|
1944
1988
|
gen: z.array(z.object({
|
|
1945
|
-
generator: z.
|
|
1989
|
+
generator: z.string().refine((val) => isValidGenerator(val), { message: `Invalid generator. Valid generators: ${validGenerators.join(", ")}` }).describe(`Code generation generator: ${SHORT_GENERATOR_NAMES.join(", ")}`),
|
|
1946
1990
|
name: z.string().optional().describe("Override the generated class/module name"),
|
|
1947
1991
|
template: z.string().optional().describe("Custom Handlebars template path"),
|
|
1948
1992
|
output: z.string().min(1, "Output path is required").describe("Output file or directory path"),
|
|
@@ -1960,15 +2004,36 @@ var ExtraVariable = class {
|
|
|
1960
2004
|
this.value = value;
|
|
1961
2005
|
}
|
|
1962
2006
|
};
|
|
1963
|
-
function createExtraVariables(sources) {
|
|
2007
|
+
function createExtraVariables(sources, suppressLogging = false) {
|
|
1964
2008
|
return sources.map((source) => {
|
|
1965
2009
|
const path = source.path;
|
|
1966
2010
|
const resolvedPath = path.replace("$HOME", homedir());
|
|
1967
2011
|
const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
|
|
1968
|
-
consola.info("Extra variable:", varName, resolvedPath);
|
|
2012
|
+
if (!suppressLogging) consola.info("Extra variable:", varName, resolvedPath);
|
|
1969
2013
|
return new ExtraVariable(varName, `'${resolvedPath}'`);
|
|
1970
2014
|
});
|
|
1971
2015
|
}
|
|
2016
|
+
function buildProjectFromCliOptions(options) {
|
|
2017
|
+
if (!isValidGenerator(options.generator)) {
|
|
2018
|
+
const similar = findSimilarGenerators(options.generator);
|
|
2019
|
+
const allGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
|
|
2020
|
+
throw new InvalidGeneratorError(options.generator, allGenerators, similar.length > 0 ? similar[0] : void 0);
|
|
2021
|
+
}
|
|
2022
|
+
const generatorInfo = parseGenerator(options.generator);
|
|
2023
|
+
const genConfig = {
|
|
2024
|
+
generator: options.generator,
|
|
2025
|
+
output: options.output || "."
|
|
2026
|
+
};
|
|
2027
|
+
if (generatorInfo.language === "java") genConfig.config = { package: "generated" };
|
|
2028
|
+
return {
|
|
2029
|
+
version: 1,
|
|
2030
|
+
name: options.name || "generated",
|
|
2031
|
+
sql: [{
|
|
2032
|
+
files: options.files,
|
|
2033
|
+
gen: [genConfig]
|
|
2034
|
+
}]
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
1972
2037
|
/**
|
|
1973
2038
|
* Parse and validate project configuration with helpful error messages
|
|
1974
2039
|
*/
|
|
@@ -1990,12 +2055,12 @@ function parseProjectConfig(filePath) {
|
|
|
1990
2055
|
const obj = parsed;
|
|
1991
2056
|
if (obj.sql && Array.isArray(obj.sql)) for (let i = 0; i < obj.sql.length; i++) {
|
|
1992
2057
|
const sqlConfig = obj.sql[i];
|
|
1993
|
-
if (sqlConfig.engine && !DB_ENGINES.includes(sqlConfig.engine)) throw new InvalidEngineError(String(sqlConfig.engine), [...DB_ENGINES]);
|
|
1994
2058
|
if (sqlConfig.gen && Array.isArray(sqlConfig.gen)) for (let j = 0; j < sqlConfig.gen.length; j++) {
|
|
1995
2059
|
const genConfig = sqlConfig.gen[j];
|
|
1996
|
-
if (genConfig.generator && !
|
|
2060
|
+
if (genConfig.generator && !isValidGenerator(String(genConfig.generator))) {
|
|
1997
2061
|
const similar = findSimilarGenerators(String(genConfig.generator));
|
|
1998
|
-
|
|
2062
|
+
const allGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
|
|
2063
|
+
throw new InvalidGeneratorError(String(genConfig.generator), allGenerators, similar.length > 0 ? similar[0] : void 0);
|
|
1999
2064
|
}
|
|
2000
2065
|
}
|
|
2001
2066
|
}
|
|
@@ -2039,11 +2104,16 @@ function validateQueries(queries) {
|
|
|
2039
2104
|
};
|
|
2040
2105
|
}
|
|
2041
2106
|
}
|
|
2042
|
-
async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine) {
|
|
2107
|
+
async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false) {
|
|
2043
2108
|
await generator.beforeGenerate(projectDir, gen, queries, tables);
|
|
2044
2109
|
const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
|
|
2045
2110
|
const name = gen.name ?? basename(file, extname(file));
|
|
2046
2111
|
const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.config);
|
|
2112
|
+
if (writeToStdout) {
|
|
2113
|
+
process.stdout.write(sourceFile);
|
|
2114
|
+
if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2047
2117
|
const outputPath = getOutputPath(projectDir, name, gen, generator);
|
|
2048
2118
|
writeFileSync(outputPath, sourceFile);
|
|
2049
2119
|
consola.success(`Generated ${outputPath}`);
|
|
@@ -2051,33 +2121,11 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
|
|
|
2051
2121
|
return outputPath;
|
|
2052
2122
|
}
|
|
2053
2123
|
/**
|
|
2054
|
-
* Validate project configuration without executing queries
|
|
2124
|
+
* Validate project configuration from a Project object without executing queries
|
|
2055
2125
|
* Use this for pre-flight checks before generation
|
|
2056
2126
|
*/
|
|
2057
|
-
async function
|
|
2127
|
+
async function validateProjectFromConfig(project, projectDir) {
|
|
2058
2128
|
const errors = [];
|
|
2059
|
-
const projectDir = resolve(dirname(projectPath));
|
|
2060
|
-
let project;
|
|
2061
|
-
try {
|
|
2062
|
-
project = parseProjectConfig(projectPath);
|
|
2063
|
-
} catch (e) {
|
|
2064
|
-
if (e instanceof SqgError) return {
|
|
2065
|
-
valid: false,
|
|
2066
|
-
errors: [{
|
|
2067
|
-
code: e.code,
|
|
2068
|
-
message: e.message,
|
|
2069
|
-
suggestion: e.suggestion,
|
|
2070
|
-
context: e.context
|
|
2071
|
-
}]
|
|
2072
|
-
};
|
|
2073
|
-
return {
|
|
2074
|
-
valid: false,
|
|
2075
|
-
errors: [{
|
|
2076
|
-
code: "UNKNOWN_ERROR",
|
|
2077
|
-
message: String(e)
|
|
2078
|
-
}]
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
2129
|
const sqlFiles = [];
|
|
2082
2130
|
const generators = [];
|
|
2083
2131
|
for (const sql of project.sql) for (const sqlFile of sql.files) {
|
|
@@ -2091,15 +2139,15 @@ async function validateProject(projectPath) {
|
|
|
2091
2139
|
});
|
|
2092
2140
|
for (const gen of sql.gen) {
|
|
2093
2141
|
generators.push(gen.generator);
|
|
2094
|
-
if (!
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2142
|
+
if (!isValidGenerator(gen.generator)) {
|
|
2143
|
+
const similar = findSimilarGenerators(gen.generator);
|
|
2144
|
+
errors.push({
|
|
2145
|
+
code: "INVALID_GENERATOR",
|
|
2146
|
+
message: `Invalid generator '${gen.generator}'`,
|
|
2147
|
+
suggestion: similar.length > 0 ? `Did you mean '${similar[0]}'?` : `Valid generators: ${SHORT_GENERATOR_NAMES.join(", ")}`,
|
|
2148
|
+
context: { generator: gen.generator }
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2103
2151
|
}
|
|
2104
2152
|
}
|
|
2105
2153
|
return {
|
|
@@ -2114,53 +2162,96 @@ async function validateProject(projectPath) {
|
|
|
2114
2162
|
};
|
|
2115
2163
|
}
|
|
2116
2164
|
/**
|
|
2117
|
-
*
|
|
2165
|
+
* Validate project configuration from a YAML file without executing queries
|
|
2166
|
+
* Use this for pre-flight checks before generation
|
|
2118
2167
|
*/
|
|
2119
|
-
async function
|
|
2168
|
+
async function validateProject(projectPath) {
|
|
2120
2169
|
const projectDir = resolve(dirname(projectPath));
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2170
|
+
let project;
|
|
2171
|
+
try {
|
|
2172
|
+
project = parseProjectConfig(projectPath);
|
|
2173
|
+
} catch (e) {
|
|
2174
|
+
if (e instanceof SqgError) return {
|
|
2175
|
+
valid: false,
|
|
2176
|
+
errors: [{
|
|
2177
|
+
code: e.code,
|
|
2178
|
+
message: e.message,
|
|
2179
|
+
suggestion: e.suggestion,
|
|
2180
|
+
context: e.context
|
|
2181
|
+
}]
|
|
2182
|
+
};
|
|
2183
|
+
return {
|
|
2184
|
+
valid: false,
|
|
2185
|
+
errors: [{
|
|
2186
|
+
code: "UNKNOWN_ERROR",
|
|
2187
|
+
message: String(e)
|
|
2188
|
+
}]
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
return await validateProjectFromConfig(project, projectDir);
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Process a project configuration and generate code from a Project object
|
|
2195
|
+
*/
|
|
2196
|
+
async function processProjectFromConfig(project, projectDir, writeToStdout = false) {
|
|
2197
|
+
const originalLevel = consola.level;
|
|
2198
|
+
if (writeToStdout) consola.level = LogLevels.silent;
|
|
2199
|
+
try {
|
|
2200
|
+
const extraVariables = createExtraVariables(project.sources ?? [], writeToStdout);
|
|
2201
|
+
const files = [];
|
|
2202
|
+
for (const sql of project.sql) {
|
|
2203
|
+
const gensByEngine = /* @__PURE__ */ new Map();
|
|
2157
2204
|
for (const gen of sql.gen) {
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2205
|
+
const engine = getGeneratorEngine(gen.generator);
|
|
2206
|
+
if (!gensByEngine.has(engine)) gensByEngine.set(engine, []);
|
|
2207
|
+
gensByEngine.get(engine).push(gen);
|
|
2208
|
+
}
|
|
2209
|
+
for (const sqlFile of sql.files) {
|
|
2210
|
+
const fullPath = join(projectDir, sqlFile);
|
|
2211
|
+
if (!existsSync(fullPath)) throw new FileNotFoundError(fullPath, projectDir);
|
|
2212
|
+
let queries;
|
|
2213
|
+
let tables;
|
|
2214
|
+
try {
|
|
2215
|
+
const parseResult = parseSQLQueries(fullPath, extraVariables);
|
|
2216
|
+
queries = parseResult.queries;
|
|
2217
|
+
tables = parseResult.tables;
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
if (e instanceof SqgError) throw e;
|
|
2220
|
+
throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
|
|
2221
|
+
}
|
|
2222
|
+
for (const [engine, gens] of gensByEngine) {
|
|
2223
|
+
try {
|
|
2224
|
+
const dbEngine = getDatabaseEngine(engine);
|
|
2225
|
+
await dbEngine.initializeDatabase(queries);
|
|
2226
|
+
await dbEngine.executeQueries(queries);
|
|
2227
|
+
if (tables.length > 0) await dbEngine.introspectTables(tables);
|
|
2228
|
+
validateQueries(queries);
|
|
2229
|
+
await dbEngine.close();
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
if (e instanceof SqgError) throw e;
|
|
2232
|
+
throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${engine}'`, {
|
|
2233
|
+
file: sqlFile,
|
|
2234
|
+
engine
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
for (const gen of gens) {
|
|
2238
|
+
const outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries, tables, engine, writeToStdout);
|
|
2239
|
+
if (outputPath !== null) files.push(outputPath);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2160
2242
|
}
|
|
2161
2243
|
}
|
|
2244
|
+
return files;
|
|
2245
|
+
} finally {
|
|
2246
|
+
if (writeToStdout) consola.level = originalLevel;
|
|
2162
2247
|
}
|
|
2163
|
-
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Process a project configuration and generate code from a YAML file
|
|
2251
|
+
*/
|
|
2252
|
+
async function processProject(projectPath) {
|
|
2253
|
+
const projectDir = resolve(dirname(projectPath));
|
|
2254
|
+
return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false);
|
|
2164
2255
|
}
|
|
2165
2256
|
|
|
2166
2257
|
//#endregion
|
|
@@ -2169,14 +2260,10 @@ async function processProject(projectPath) {
|
|
|
2169
2260
|
* SQG Project Initialization - Creates new SQG projects with example files
|
|
2170
2261
|
*/
|
|
2171
2262
|
/**
|
|
2172
|
-
* Get the default generator for
|
|
2263
|
+
* Get the default generator for a language preference
|
|
2173
2264
|
*/
|
|
2174
|
-
function getDefaultGenerator(
|
|
2175
|
-
return
|
|
2176
|
-
sqlite: "typescript/better-sqlite3",
|
|
2177
|
-
duckdb: "typescript/duckdb",
|
|
2178
|
-
postgres: "java/jdbc"
|
|
2179
|
-
}[engine];
|
|
2265
|
+
function getDefaultGenerator() {
|
|
2266
|
+
return "typescript/sqlite";
|
|
2180
2267
|
}
|
|
2181
2268
|
/**
|
|
2182
2269
|
* Generate example SQL content based on engine
|
|
@@ -2374,21 +2461,8 @@ DELETE FROM posts WHERE id = \${id};
|
|
|
2374
2461
|
/**
|
|
2375
2462
|
* Generate sqg.yaml configuration
|
|
2376
2463
|
*/
|
|
2377
|
-
function getConfigYaml(
|
|
2378
|
-
|
|
2379
|
-
const config = {
|
|
2380
|
-
version: 1,
|
|
2381
|
-
name: "my-project",
|
|
2382
|
-
sql: [{
|
|
2383
|
-
engine,
|
|
2384
|
-
files: ["queries.sql"],
|
|
2385
|
-
gen: [{
|
|
2386
|
-
generator,
|
|
2387
|
-
output: output.endsWith("/") ? output : `${output}/`
|
|
2388
|
-
}]
|
|
2389
|
-
}]
|
|
2390
|
-
};
|
|
2391
|
-
if (generator.startsWith("java/")) config.sql[0].gen[0].config = { package: "generated" };
|
|
2464
|
+
function getConfigYaml(generator, output) {
|
|
2465
|
+
const isJava = parseGenerator(generator).language === "java";
|
|
2392
2466
|
return `# SQG Configuration
|
|
2393
2467
|
# Generated by: sqg init
|
|
2394
2468
|
# Documentation: https://sqg.dev
|
|
@@ -2397,12 +2471,11 @@ version: 1
|
|
|
2397
2471
|
name: my-project
|
|
2398
2472
|
|
|
2399
2473
|
sql:
|
|
2400
|
-
-
|
|
2401
|
-
files:
|
|
2474
|
+
- files:
|
|
2402
2475
|
- queries.sql
|
|
2403
2476
|
gen:
|
|
2404
2477
|
- generator: ${generator}
|
|
2405
|
-
output: ${output.endsWith("/") ? output : `${output}/`}${
|
|
2478
|
+
output: ${output.endsWith("/") ? output : `${output}/`}${isJava ? `
|
|
2406
2479
|
config:
|
|
2407
2480
|
package: generated` : ""}
|
|
2408
2481
|
`;
|
|
@@ -2411,18 +2484,13 @@ sql:
|
|
|
2411
2484
|
* Initialize a new SQG project
|
|
2412
2485
|
*/
|
|
2413
2486
|
async function initProject(options) {
|
|
2414
|
-
const
|
|
2487
|
+
const generator = options.generator || getDefaultGenerator();
|
|
2415
2488
|
const output = options.output || "./generated";
|
|
2416
|
-
if (!
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
throw new InvalidGeneratorError(options.generator, Object.keys(SUPPORTED_GENERATORS), similar.length > 0 ? similar[0] : void 0);
|
|
2422
|
-
}
|
|
2423
|
-
generator = options.generator;
|
|
2424
|
-
if (!SUPPORTED_GENERATORS[generator].compatibleEngines.includes(engine)) throw new SqgError(`Generator '${generator}' is not compatible with engine '${engine}'`, "GENERATOR_ENGINE_MISMATCH", `For '${engine}', use one of: ${Object.entries(SUPPORTED_GENERATORS).filter(([_, info]) => info.compatibleEngines.includes(engine)).map(([name]) => name).join(", ")}`);
|
|
2425
|
-
} else generator = getDefaultGenerator(engine);
|
|
2489
|
+
if (!isValidGenerator(generator)) {
|
|
2490
|
+
const similar = findSimilarGenerators(generator);
|
|
2491
|
+
throw new InvalidGeneratorError(generator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
|
|
2492
|
+
}
|
|
2493
|
+
const engine = parseGenerator(generator).engine;
|
|
2426
2494
|
const configPath = "sqg.yaml";
|
|
2427
2495
|
const sqlPath = "queries.sql";
|
|
2428
2496
|
if (!options.force) {
|
|
@@ -2433,16 +2501,16 @@ async function initProject(options) {
|
|
|
2433
2501
|
mkdirSync(output, { recursive: true });
|
|
2434
2502
|
consola.success(`Created output directory: ${output}`);
|
|
2435
2503
|
}
|
|
2436
|
-
writeFileSync(configPath, getConfigYaml(
|
|
2504
|
+
writeFileSync(configPath, getConfigYaml(generator, output));
|
|
2437
2505
|
consola.success(`Created ${configPath}`);
|
|
2438
2506
|
writeFileSync(sqlPath, getExampleSql(engine));
|
|
2439
2507
|
consola.success(`Created ${sqlPath}`);
|
|
2440
2508
|
consola.box(`
|
|
2441
2509
|
SQG project initialized!
|
|
2442
2510
|
|
|
2443
|
-
Engine: ${engine}
|
|
2444
2511
|
Generator: ${generator}
|
|
2445
|
-
|
|
2512
|
+
Engine: ${engine}
|
|
2513
|
+
Output: ${output}
|
|
2446
2514
|
|
|
2447
2515
|
Next steps:
|
|
2448
2516
|
1. Edit queries.sql to add your SQL queries
|
|
@@ -2455,46 +2523,81 @@ Documentation: https://sqg.dev
|
|
|
2455
2523
|
|
|
2456
2524
|
//#endregion
|
|
2457
2525
|
//#region src/sqg.ts
|
|
2458
|
-
const version = process.env.npm_package_version ?? "0.
|
|
2526
|
+
const version = process.env.npm_package_version ?? "0.5.0";
|
|
2459
2527
|
const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
|
|
2460
2528
|
consola.level = LogLevels.info;
|
|
2461
2529
|
const program = new Command().name("sqg").description(`${description}
|
|
2462
2530
|
|
|
2463
2531
|
Generate type-safe database access code from annotated SQL files.
|
|
2464
2532
|
|
|
2465
|
-
Supported Engines:
|
|
2466
|
-
${formatEnginesHelp()}
|
|
2467
|
-
|
|
2468
2533
|
Supported Generators:
|
|
2469
|
-
${formatGeneratorsHelp()}`).version(version, "-v, --version", "output the version number").option("--verbose", "Enable debug logging (shows SQL execution details)").option("--format <format>", "Output format: text (default) or json", "text").option("--validate", "Validate configuration without generating code").
|
|
2470
|
-
|
|
2534
|
+
${formatGeneratorsHelp()}`).version(version, "-v, --version", "output the version number").option("--verbose", "Enable debug logging (shows SQL execution details)").option("--format <format>", "Output format: text (default) or json", "text").option("--validate", "Validate configuration without generating code").option("--generator <generator>", `Code generation generator (${SHORT_GENERATOR_NAMES.join(", ")})`).option("--file <file>", "SQL file path (can be repeated)", (val, prev = []) => {
|
|
2535
|
+
prev.push(val);
|
|
2536
|
+
return prev;
|
|
2537
|
+
}).option("--output <path>", "Output file or directory path (optional, if omitted writes to stdout)").option("--name <name>", "Project name (optional, defaults to 'generated')").showHelpAfterError().showSuggestionAfterError();
|
|
2538
|
+
program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omit to use CLI options").hook("preAction", (thisCommand) => {
|
|
2471
2539
|
const opts = thisCommand.opts();
|
|
2472
2540
|
if (opts.verbose) consola.level = LogLevels.debug;
|
|
2473
2541
|
if (opts.format === "json") consola.level = LogLevels.silent;
|
|
2474
2542
|
}).action(async (projectPath, options) => {
|
|
2475
2543
|
try {
|
|
2476
|
-
if (
|
|
2477
|
-
|
|
2478
|
-
if (options.
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2544
|
+
if (!projectPath) {
|
|
2545
|
+
if (!options.generator) throw new SqgError("Missing required option: --generator", "CONFIG_VALIDATION_ERROR", `Specify a code generation generator: ${SHORT_GENERATOR_NAMES.join(", ")}`);
|
|
2546
|
+
if (!options.file || options.file.length === 0) throw new SqgError("Missing required option: --file", "CONFIG_VALIDATION_ERROR", "Specify at least one SQL file with --file <path>");
|
|
2547
|
+
const project = buildProjectFromCliOptions({
|
|
2548
|
+
generator: options.generator,
|
|
2549
|
+
files: options.file,
|
|
2550
|
+
output: options.output,
|
|
2551
|
+
name: options.name
|
|
2552
|
+
});
|
|
2553
|
+
const projectDir = process.cwd();
|
|
2554
|
+
const writeToStdout = !options.output;
|
|
2555
|
+
if (options.validate) {
|
|
2556
|
+
const result = await validateProjectFromConfig(project, projectDir);
|
|
2557
|
+
if (options.format === "json") console.log(JSON.stringify(result, null, 2));
|
|
2558
|
+
else if (result.valid) {
|
|
2559
|
+
consola.success("Configuration is valid");
|
|
2560
|
+
consola.info(`Project: ${result.project?.name}`);
|
|
2561
|
+
consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
|
|
2562
|
+
consola.info(`Generators: ${result.generators?.join(", ")}`);
|
|
2563
|
+
} else {
|
|
2564
|
+
consola.error("Validation failed");
|
|
2565
|
+
for (const error of result.errors || []) {
|
|
2566
|
+
consola.error(` ${error.message}`);
|
|
2567
|
+
if (error.suggestion) consola.info(` Suggestion: ${error.suggestion}`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
exit(result.valid ? 0 : 1);
|
|
2571
|
+
}
|
|
2572
|
+
const files = await processProjectFromConfig(project, projectDir, writeToStdout);
|
|
2573
|
+
if (options.format === "json" && !writeToStdout) console.log(JSON.stringify({
|
|
2574
|
+
status: "success",
|
|
2575
|
+
generatedFiles: files
|
|
2576
|
+
}));
|
|
2577
|
+
} else {
|
|
2578
|
+
if (options.validate) {
|
|
2579
|
+
const result = await validateProject(projectPath);
|
|
2580
|
+
if (options.format === "json") console.log(JSON.stringify(result, null, 2));
|
|
2581
|
+
else if (result.valid) {
|
|
2582
|
+
consola.success("Configuration is valid");
|
|
2583
|
+
consola.info(`Project: ${result.project?.name}`);
|
|
2584
|
+
consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
|
|
2585
|
+
consola.info(`Generators: ${result.generators?.join(", ")}`);
|
|
2586
|
+
} else {
|
|
2587
|
+
consola.error("Validation failed");
|
|
2588
|
+
for (const error of result.errors || []) {
|
|
2589
|
+
consola.error(` ${error.message}`);
|
|
2590
|
+
if (error.suggestion) consola.info(` Suggestion: ${error.suggestion}`);
|
|
2591
|
+
}
|
|
2489
2592
|
}
|
|
2593
|
+
exit(result.valid ? 0 : 1);
|
|
2490
2594
|
}
|
|
2491
|
-
|
|
2595
|
+
const files = await processProject(projectPath);
|
|
2596
|
+
if (options.format === "json") console.log(JSON.stringify({
|
|
2597
|
+
status: "success",
|
|
2598
|
+
generatedFiles: files
|
|
2599
|
+
}));
|
|
2492
2600
|
}
|
|
2493
|
-
const files = await processProject(projectPath);
|
|
2494
|
-
if (options.format === "json") console.log(JSON.stringify({
|
|
2495
|
-
status: "success",
|
|
2496
|
-
generatedFiles: files
|
|
2497
|
-
}));
|
|
2498
2601
|
} catch (err) {
|
|
2499
2602
|
if (options.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
|
|
2500
2603
|
else if (err instanceof SqgError) {
|
|
@@ -2505,7 +2608,7 @@ program.argument("<project>", "Path to the project YAML config (sqg.yaml)").hook
|
|
|
2505
2608
|
exit(1);
|
|
2506
2609
|
}
|
|
2507
2610
|
});
|
|
2508
|
-
program.command("init").description("Initialize a new SQG project with example configuration").option("-
|
|
2611
|
+
program.command("init").description("Initialize a new SQG project with example configuration").option("-t, --generator <generator>", `Code generation generator (${SHORT_GENERATOR_NAMES.join(", ")})`, "typescript/sqlite").option("-o, --output <dir>", "Output directory for generated files", "./generated").option("-f, --force", "Overwrite existing files").action(async (options) => {
|
|
2509
2612
|
const parentOpts = program.opts();
|
|
2510
2613
|
try {
|
|
2511
2614
|
await initProject(options);
|