loom-weaver 1.0.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/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Loom CLI
2
+
3
+ > Turn SQL `CREATE TABLE` scripts and Prisma schemas into clean, realistic seed data.
4
+
5
+ Loom CLI parses your SQL DDL (`.sql`) or Prisma model (`.prisma`) files and generates realistic mock data. It supports 5 major SQL dialects (PostgreSQL, MySQL, SQLite, MS SQL Server, Oracle) and can export to SQL `INSERT` statements, CSV, JSON, or a Prisma Client seed script.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Dual Parsing**: Handles both raw SQL `CREATE TABLE` definitions and Prisma `schema.prisma` models.
12
+ - **Multi-Table Extraction**: Automatically detects all tables/models in a file and lets you choose which one to generate data for (or specify with `-t`).
13
+ - **5 Database Dialects**: Adjusts quoting, string escaping, booleans, binary hex literals, and arrays for Postgres, MySQL, SQLite, T-SQL, and Oracle.
14
+ - **4 Export Formats**: Output as SQL `INSERT` queries, RFC 4180 CSV, formatted JSON, or a runnable `seed.js` Prisma script.
15
+ - **Interactive Terminal UI**: Step-by-step terminal prompts using arrow keys, with live path validation (no crash on typos).
16
+ - **Smart Extensions**: Pass `-o mydata` and Loom auto-detects `.json`, `.csv`, `.sql`, or `.js` based on your chosen format.
17
+ - **Aesthetic Overwrite Safeguard**: If an output file exists and you choose not to overwrite, Loom automatically saves to a fresh aesthetic filename (like `mango.sql` or `aurora.sql`).
18
+ - **Primary Key Safety**: Detects `AUTO_INCREMENT`, `SERIAL`, `IDENTITY`, `nextval()`, and `@default(autoincrement())` fields and excludes them from `INSERT`s so the database auto-generates IDs naturally.
19
+ - **Smart Data Generators**: Generates realistic names, emails, addresses, phones, CGPAs, UUIDs, XML, JSON, OpenGIS spatial geometries (`ST_GeomFromText`), network IPs, MAC addresses, and string length-checked `VARCHAR(N)` fields.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install -g loom-weaver-cli
27
+ ```
28
+
29
+ Or for local development:
30
+ ```bash
31
+ git clone https://github.com/your-username/loom-cli.git
32
+ cd loom-cli
33
+ npm install
34
+ npm link
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ### Interactive Prompt
42
+ Run `loom weave` to launch interactive mode:
43
+
44
+ ```bash
45
+ loom weave
46
+ ```
47
+
48
+ Example session:
49
+ ```text
50
+ ? Schema file path: schema.sql
51
+ ? Select table to weave data for: users
52
+ ? Select target export format: SQL INSERT statements (.sql)
53
+ ? Select target SQL dialect: PostgreSQL
54
+ ? Number of rows to generate: 25
55
+ ? Output file name: seed.sql
56
+
57
+ ✔ Success! Woven 25 rows for 'users' into seed.sql
58
+ ```
59
+
60
+ ### CLI Flags (Non-interactive)
61
+ Skip prompts with `-s` and pass options directly:
62
+
63
+ ```bash
64
+ # Export 50 rows for MySQL
65
+ loom weave schema.sql -d mysql -r 50 -o mysql_seed.sql -s
66
+
67
+ # Target a specific table in a multi-table SQL script
68
+ loom weave migration.sql -t users -r 50 -o users.sql -s
69
+
70
+ # Export 100 rows to CSV (auto-appends .csv)
71
+ loom weave schema.sql -f csv -r 100 -o users -s
72
+
73
+ # Export 25 rows to JSON (auto-appends .json)
74
+ loom weave schema.sql -f json -r 25 -o users -s
75
+
76
+ # Generate a Prisma Client seed script (seed.js)
77
+ loom weave schema.prisma -f prisma -r 50 -o seed -s
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Options
83
+
84
+ | Flag | Short | Description | Default |
85
+ | :--- | :--- | :--- | :--- |
86
+ | `--table <name>` | `-t` | Table or model name to extract | First table in schema |
87
+ | `--rows <number>` | `-r` | Number of rows to generate | `10` |
88
+ | `--output <file>` | `-o` | Output file name | `seed.sql` (or `.csv`/`.json`/`.js`) |
89
+ | `--format <type>` | `-f` | Output format (`sql`, `csv`, `json`, `prisma`) | `sql` |
90
+ | `--dialect <name>` | `-d` | SQL engine (`postgres`, `mysql`, `sqlite`, `mssql`, `oracle`) | `postgres` |
91
+ | `--skip-prompts` | `-s` | Skip interactive prompt and use flags | `false` |
92
+
93
+ ---
94
+
95
+ ## Supported Data Types
96
+
97
+ | Category | Schema Types | Example Generated Output |
98
+ | :--- | :--- | :--- |
99
+ | **Identifiers** | `UUID`, `cuid()`, `ulid()`, `@db.Uuid`, `*_id` | `'89dcaf73-7010-484a-bef1-80cd389faa45'` |
100
+ | **Auto-Increment** | `SERIAL`, `BIGSERIAL`, `AUTO_INCREMENT`, `IDENTITY`, `nextval()` | *Omitted from INSERT list* |
101
+ | **Integers** | `INT`, `BIGINT`, `SMALLINT`, `TINYINT` | `42`, `8921` |
102
+ | **Decimals & Floats**| `DECIMAL(10,2)`, `NUMERIC`, `FLOAT`, `DOUBLE PRECISION`, `REAL` | `154.50`, `3.85` |
103
+ | **Booleans** | `BOOLEAN`, `BIT`, `is_*`, `has_*` | `TRUE` / `FALSE` (or `1` / `0`) |
104
+ | **Dates & Times** | `DATE`, `TIME`, `TIMETZ`, `TIMESTAMP`, `TIMESTAMPTZ` | `'2026-03-31'`, `'13:39:24'`, `'2026-02-06 10:28:36'` |
105
+ | **Intervals** | `INTERVAL`, `*_duration` | `'2 hours 30 mins'`, `'1 day'` |
106
+ | **JSON** | `JSON`, `JSONB`, Prisma `Json` | `'{"theme":"dark", "views":150}'` |
107
+ | **XML** | `XML`, `@db.Xml` | `'<document><id>123</id><content>data</content></document>'` |
108
+ | **Binary** | `BYTEA`, `BLOB`, `BINARY`, Prisma `Bytes` | Hex format per dialect (`'\x...'`, `X'...'`, `0x...`) |
109
+ | **Spatial / GIS** | `POINT`, `LSEG`, `LINESTRING`, `POLYGON` | `ST_GeomFromText('POINT(-87.39 41.88)')` |
110
+ | **Network** | `INET`, `CIDR`, `MACADDR` | `'192.168.1.1'`, `'00:1A:2B:3C:4D:5E'` |
111
+ | **Enums** | SQL `ENUM(...)` or Prisma `enum Role { ... }` | Selected from enum values (`'ADMIN'`, `'USER'`) |
112
+ | **Arrays / Lists** | Postgres `TEXT[]`, `INT[]` or Prisma `String[]` | Native array syntax (`'{"a", "b"}'` or `JSON_ARRAY(...)`) |
113
+
114
+ ---
115
+
116
+ ## Programmatic API
117
+
118
+ You can also use Loom as a library inside your Node.js code:
119
+
120
+ ```javascript
121
+ import { weave, parseSchema, generateRow, formatData } from 'loom-weaver-cli';
122
+
123
+ // Weave directly from a schema file
124
+ const result = weave('schema.sql', {
125
+ table: 'users',
126
+ rows: 50,
127
+ format: 'json',
128
+ output: 'users' // Auto-resolves to users.json
129
+ });
130
+
131
+ console.log(`Generated ${result.rowCount} rows into ${result.outputFile}`);
132
+
133
+ // Low-level schema parsing & row generation
134
+ const schemaText = `
135
+ CREATE TABLE users (
136
+ id UUID PRIMARY KEY,
137
+ name VARCHAR(100),
138
+ email VARCHAR(100)
139
+ );
140
+ `;
141
+
142
+ const parsed = parseSchema(schemaText, false); // false = SQL
143
+ const rows = Array.from({ length: 5 }, () => generateRow(parsed.columns));
144
+
145
+ const sql = formatData(parsed.tableName, rows, {
146
+ format: 'sql',
147
+ dialect: 'mysql'
148
+ });
149
+
150
+ console.log(sql);
151
+ ```
152
+
153
+ ---
154
+
155
+ ## License
156
+
157
+ ISC
package/bin/loom.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCLI } from '../src/cli.js';
4
+
5
+ runCLI();
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "loom-weaver",
3
+ "version": "1.0.0",
4
+ "description": "A CLI to weave mock data from your database schemas",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "loom": "bin/loom.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "keywords": [
18
+ "cli",
19
+ "mock-data",
20
+ "faker",
21
+ "sql",
22
+ "prisma",
23
+ "generator",
24
+ "postgres",
25
+ "mysql",
26
+ "sqlite",
27
+ "seed"
28
+ ],
29
+ "author": "",
30
+ "license": "ISC",
31
+ "type": "module",
32
+ "dependencies": {
33
+ "@faker-js/faker": "^10.5.0",
34
+ "@inquirer/prompts": "^8.5.2",
35
+ "chalk": "^5.6.2",
36
+ "commander": "^15.0.0",
37
+ "ora": "^9.4.1"
38
+ }
39
+ }
package/src/cli.js ADDED
@@ -0,0 +1,248 @@
1
+ import { program } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { input, select } from '@inquirer/prompts';
7
+ import { weave } from './index.js';
8
+ import { stripSqlComments } from './parsers/sqlParser.js';
9
+ import { getSchemaTableNames } from './parsers/index.js';
10
+
11
+ const AESTHETIC_WORDS = [
12
+ 'mango', 'aurora', 'velvet', 'nebula', 'breeze',
13
+ 'cascade', 'horizon', 'zenith', 'solar', 'lunar',
14
+ 'prism', 'echo', 'ember', 'drift', 'orbit'
15
+ ];
16
+
17
+ function getUniqueAestheticFilename(ext = '.sql') {
18
+ for (const word of AESTHETIC_WORDS) {
19
+ const candidate = `${word}${ext}`;
20
+ if (!fs.existsSync(candidate)) {
21
+ return candidate;
22
+ }
23
+ }
24
+ let count = 1;
25
+ while (fs.existsSync(`seed_${count}${ext}`)) {
26
+ count++;
27
+ }
28
+ return `seed_${count}${ext}`;
29
+ }
30
+
31
+ function validateSchemaFile(filePath) {
32
+ if (!fs.existsSync(filePath)) {
33
+ throw new Error(`File '${filePath}' does not exist.`);
34
+ }
35
+
36
+ const isPrisma = filePath.endsWith('.prisma');
37
+ const rawText = fs.readFileSync(filePath, 'utf-8');
38
+
39
+ if (isPrisma) {
40
+ const cleanText = rawText.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*/g, '');
41
+ const modelMatch = cleanText.match(/model\s+(\w+)\s*{([\s\S]+?)}/i);
42
+ if (!modelMatch) {
43
+ throw new Error(`File '${filePath}' does not contain a valid Prisma model.`);
44
+ }
45
+ } else {
46
+ const cleanText = stripSqlComments(rawText);
47
+ const tableMatch = cleanText.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:[`"]?[\w]+[`"]?\.)?[`"]?[\w]+[`"]?|\"[^\"]+\"|`[^`]+`)\s*\(([\s\S]+?)\);/i);
48
+ if (!tableMatch) {
49
+ throw new Error(`File '${filePath}' does not contain a valid CREATE TABLE statement.`);
50
+ }
51
+ }
52
+ }
53
+
54
+ program
55
+ .name('loom')
56
+ .description('A CLI to weave mock data from your database schemas')
57
+ .version('1.0.0');
58
+
59
+ program
60
+ .command('ping')
61
+ .description('Check if the CLI is working')
62
+ .action(() => {
63
+ console.log(chalk.green('Pong! Loom is alive and well.'));
64
+ });
65
+
66
+ program
67
+ .command('weave [file]')
68
+ .description('Read a schema file to prepare for data generation')
69
+ .option('-r, --rows <number>', 'Number of rows to generate')
70
+ .option('-o, --output <filename>', 'Output file name')
71
+ .option('-f, --format <type>', 'Export format (sql, csv, json, prisma)')
72
+ .option('-d, --dialect <name>', 'Target SQL dialect (postgres, mysql, sqlite, mssql, oracle)')
73
+ .option('-t, --table <name>', 'Target table or model name in multi-entity schema')
74
+ .option('-s, --skip-prompts', 'Skip interactive prompts and use defaults or passed flags')
75
+ .action(async (file, options) => {
76
+ let schemaFile = file;
77
+ let rowCount = options.rows;
78
+ let outputFile = options.output;
79
+ let format = options.format;
80
+ let dialect = options.dialect;
81
+ let targetTable = options.table;
82
+ const skipPrompts = options.skipPrompts;
83
+
84
+ if (!skipPrompts) {
85
+ try {
86
+ if (!schemaFile) {
87
+ schemaFile = await input({
88
+ message: 'Schema file path:',
89
+ validate: (val) => {
90
+ const trimmed = val.trim();
91
+ if (!trimmed) return 'Schema file path is required.';
92
+ try {
93
+ validateSchemaFile(trimmed);
94
+ return true;
95
+ } catch (err) {
96
+ return err.message;
97
+ }
98
+ }
99
+ });
100
+ schemaFile = schemaFile.trim();
101
+ } else {
102
+ try {
103
+ validateSchemaFile(schemaFile);
104
+ } catch (err) {
105
+ console.log(chalk.red(`Error: ${err.message}`));
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ // If schema has multiple tables/models, prompt user to select target entity
111
+ const schemaContent = fs.readFileSync(schemaFile, 'utf-8');
112
+ const isPrismaFile = schemaFile.endsWith('.prisma');
113
+ const availableTables = getSchemaTableNames(schemaContent, isPrismaFile);
114
+
115
+ if (availableTables.length > 1 && !targetTable) {
116
+ targetTable = await select({
117
+ message: `Select ${isPrismaFile ? 'model' : 'table'} to weave data for:`,
118
+ choices: availableTables.map(t => ({ name: t, value: t })),
119
+ default: availableTables[0]
120
+ });
121
+ }
122
+
123
+ if (!format) {
124
+ format = await select({
125
+ message: 'Select target export format:',
126
+ choices: [
127
+ { name: 'SQL INSERT statements (.sql)', value: 'sql' },
128
+ { name: 'CSV data file (.csv)', value: 'csv' },
129
+ { name: 'JSON data file (.json)', value: 'json' },
130
+ { name: 'Prisma Client seed script (.js)', value: 'prisma' }
131
+ ],
132
+ default: 'sql'
133
+ });
134
+ }
135
+
136
+ if (format === 'sql' && !dialect) {
137
+ dialect = await select({
138
+ message: 'Select target SQL dialect:',
139
+ choices: [
140
+ { name: 'PostgreSQL', value: 'postgres' },
141
+ { name: 'MySQL / MariaDB', value: 'mysql' },
142
+ { name: 'SQLite', value: 'sqlite' },
143
+ { name: 'MS SQL Server (T-SQL)', value: 'mssql' },
144
+ { name: 'Oracle Database', value: 'oracle' }
145
+ ],
146
+ default: 'postgres'
147
+ });
148
+ }
149
+
150
+ if (!rowCount) {
151
+ const answer = await input({
152
+ message: 'Number of rows to generate:',
153
+ default: '10'
154
+ });
155
+ rowCount = answer.trim() || '10';
156
+ }
157
+
158
+ let defaultExt = '.sql';
159
+ if (format === 'csv') defaultExt = '.csv';
160
+ else if (format === 'json') defaultExt = '.json';
161
+ else if (format === 'prisma') defaultExt = '.js';
162
+
163
+ if (!outputFile) {
164
+ const defaultFileName = `seed${defaultExt}`;
165
+ const answer = await input({
166
+ message: 'Output file name:',
167
+ default: defaultFileName
168
+ });
169
+ outputFile = answer.trim() || defaultFileName;
170
+ }
171
+
172
+ // Auto-append format extension if no file extension was specified
173
+ if (!path.extname(outputFile)) {
174
+ outputFile = `${outputFile}${defaultExt}`;
175
+ }
176
+
177
+ // Check if output file already exists and contains data
178
+ if (fs.existsSync(outputFile)) {
179
+ const stats = fs.statSync(outputFile);
180
+ if (stats.size > 0) {
181
+ const ext = path.extname(outputFile) || defaultExt;
182
+ const alternativeFile = getUniqueAestheticFilename(ext);
183
+
184
+ const shouldOverwrite = await select({
185
+ message: `Output file '${outputFile}' already exists. Overwrite?`,
186
+ choices: [
187
+ { name: `No (Generate to a new file instead)`, value: false },
188
+ { name: `Yes (Overwrite ${outputFile})`, value: true }
189
+ ],
190
+ default: false
191
+ });
192
+
193
+ if (!shouldOverwrite) {
194
+ outputFile = alternativeFile;
195
+ }
196
+ }
197
+ }
198
+ } catch (err) {
199
+ if (err.name === 'ExitPromptError' || err.name === 'AbortError' || err.code === 'ABORT_ERR') {
200
+ console.log(chalk.yellow('\nOperation cancelled.'));
201
+ process.exit(0);
202
+ }
203
+ throw err;
204
+ }
205
+ } else {
206
+ if (!schemaFile) {
207
+ console.log(chalk.red('Error: Schema file path is required when skipping prompts.'));
208
+ process.exit(1);
209
+ }
210
+
211
+ try {
212
+ validateSchemaFile(schemaFile);
213
+ } catch (err) {
214
+ console.log(chalk.red(`Error: ${err.message}`));
215
+ process.exit(1);
216
+ }
217
+
218
+ format = format || 'sql';
219
+ dialect = dialect || 'postgres';
220
+ rowCount = rowCount || '10';
221
+
222
+ let defaultExt = '.sql';
223
+ if (format === 'csv') defaultExt = '.csv';
224
+ else if (format === 'json') defaultExt = '.json';
225
+ else if (format === 'prisma') defaultExt = '.js';
226
+
227
+ if (!outputFile) {
228
+ outputFile = `seed${defaultExt}`;
229
+ } else if (!path.extname(outputFile)) {
230
+ outputFile = `${outputFile}${defaultExt}`;
231
+ }
232
+ }
233
+
234
+ const spinner = ora(`Reading ${schemaFile}...`).start();
235
+
236
+ try {
237
+ spinner.text = 'Parsing schema & weaving mock data...';
238
+ const result = weave(schemaFile, { rows: rowCount, output: outputFile, format, dialect, table: targetTable });
239
+
240
+ spinner.succeed(chalk.green(`Success! Woven ${result.rowCount} rows for '${result.tableName}' into ${result.outputFile}`));
241
+ } catch (error) {
242
+ spinner.fail(chalk.red(`Error: ${error.message || "Could not process file."}`));
243
+ }
244
+ });
245
+
246
+ export function runCLI() {
247
+ program.parse(process.argv);
248
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Formats mock data objects into RFC 4180 compliant CSV text.
3
+ * @param {Array<Record<string, any>>} mockData - Array of mock data row objects.
4
+ * @returns {string} CSV text.
5
+ */
6
+ export function generateCSV(mockData) {
7
+ if (!mockData || mockData.length === 0) return '';
8
+
9
+ const headers = Object.keys(mockData[0]);
10
+ const lines = [headers.join(',')];
11
+
12
+ mockData.forEach(row => {
13
+ const rowCells = headers.map(header => {
14
+ let val = row[header];
15
+ if (val === null || val === undefined) return '';
16
+
17
+ if (val && typeof val === 'object') {
18
+ if (val.rawSql) {
19
+ const geomMatch = val.rawSql.match(/ST_GeomFromText\('([^']+)'\)/i);
20
+ val = geomMatch ? geomMatch[1] : val.rawSql;
21
+ } else if (val.rawHex) {
22
+ val = val.rawHex;
23
+ } else {
24
+ val = JSON.stringify(val);
25
+ }
26
+ } else if (Array.isArray(val)) {
27
+ val = JSON.stringify(val);
28
+ }
29
+
30
+ val = String(val);
31
+ if (val.includes(',') || val.includes('"') || val.includes('\n') || val.includes('\r')) {
32
+ return `"${val.replace(/"/g, '""')}"`;
33
+ }
34
+ return val;
35
+ });
36
+
37
+ lines.push(rowCells.join(','));
38
+ });
39
+
40
+ return lines.join('\n');
41
+ }
@@ -0,0 +1,62 @@
1
+ export const DIALECTS = {
2
+ postgres: {
3
+ name: 'PostgreSQL',
4
+ quoteIdentifier: (name) => `"${name}"`,
5
+ formatBoolean: (val) => (val ? 'TRUE' : 'FALSE'),
6
+ formatBinary: (hex) => `'\\x${hex}'`,
7
+ escapeString: (str) => str.replace(/'/g, "''"),
8
+ formatArray: (val) => {
9
+ const elements = val.map(el => typeof el === 'string' ? `"${el.replace(/"/g, '\\"')}"` : el);
10
+ return "'{" + elements.join(', ') + "}'";
11
+ }
12
+ },
13
+ mysql: {
14
+ name: 'MySQL',
15
+ quoteIdentifier: (name) => `\`${name}\``,
16
+ formatBoolean: (val) => (val ? 'TRUE' : 'FALSE'),
17
+ formatBinary: (hex) => `X'${hex}'`,
18
+ escapeString: (str) => str.replace(/\\/g, '\\\\').replace(/'/g, "''"),
19
+ formatArray: (val) => {
20
+ const items = val.map(el => typeof el === 'string' ? `'${el.replace(/\\/g, '\\\\').replace(/'/g, "''")}'` : el);
21
+ return `JSON_ARRAY(${items.join(', ')})`;
22
+ }
23
+ },
24
+ sqlite: {
25
+ name: 'SQLite',
26
+ quoteIdentifier: (name) => `"${name}"`,
27
+ formatBoolean: (val) => (val ? '1' : '0'),
28
+ formatBinary: (hex) => `X'${hex}'`,
29
+ escapeString: (str) => str.replace(/'/g, "''"),
30
+ formatArray: (val) => {
31
+ const items = val.map(el => typeof el === 'string' ? `'${el.replace(/'/g, "''")}'` : el);
32
+ return `json_array(${items.join(', ')})`;
33
+ }
34
+ },
35
+ mssql: {
36
+ name: 'MS SQL Server',
37
+ quoteIdentifier: (name) => `[${name}]`,
38
+ formatBoolean: (val) => (val ? '1' : '0'),
39
+ formatBinary: (hex) => `0x${hex}`,
40
+ escapeString: (str) => str.replace(/'/g, "''"),
41
+ formatArray: (val) => {
42
+ const jsonStr = JSON.stringify(val).replace(/'/g, "''");
43
+ return `'${jsonStr}'`;
44
+ }
45
+ },
46
+ oracle: {
47
+ name: 'Oracle Database',
48
+ quoteIdentifier: (name) => `"${name.toUpperCase()}"`,
49
+ formatBoolean: (val) => (val ? '1' : '0'),
50
+ formatBinary: (hex) => `hextoraw('${hex}')`,
51
+ escapeString: (str) => str.replace(/'/g, "''"),
52
+ formatArray: (val) => {
53
+ const jsonStr = JSON.stringify(val).replace(/'/g, "''");
54
+ return `'${jsonStr}'`;
55
+ }
56
+ }
57
+ };
58
+
59
+ export function getDialect(name = 'postgres') {
60
+ const normalized = (name || 'postgres').toLowerCase().trim();
61
+ return DIALECTS[normalized] || DIALECTS.postgres;
62
+ }
@@ -0,0 +1,41 @@
1
+ import { generateSQL } from './sqlFormatter.js';
2
+ import { generateCSV } from './csvFormatter.js';
3
+ import { generateJSON } from './jsonFormatter.js';
4
+ import { generatePrismaSeed } from './prismaSeedFormatter.js';
5
+ import { DIALECTS, getDialect } from './dialects.js';
6
+
7
+ export {
8
+ generateSQL,
9
+ generateCSV,
10
+ generateJSON,
11
+ generatePrismaSeed,
12
+ DIALECTS,
13
+ getDialect
14
+ };
15
+
16
+ /**
17
+ * Formats mock data according to format type (sql, csv, json, prisma) and SQL dialect.
18
+ * @param {string} tableName - Name of table / model.
19
+ * @param {Array<Record<string, any>>} mockData - Mock rows array.
20
+ * @param {object} [options] - Options map.
21
+ * @param {string} [options.format='sql'] - Output format (sql, csv, json, prisma).
22
+ * @param {string} [options.dialect='postgres'] - Target SQL dialect (postgres, mysql, sqlite, mssql, oracle).
23
+ * @returns {string} Formatted output content.
24
+ */
25
+ export function formatData(tableName, mockData, options = {}) {
26
+ const fmt = (options.format || 'sql').toLowerCase().trim();
27
+ const dialect = options.dialect || 'postgres';
28
+
29
+ switch (fmt) {
30
+ case 'csv':
31
+ return generateCSV(mockData);
32
+ case 'json':
33
+ return generateJSON(mockData);
34
+ case 'prisma':
35
+ case 'prismaseed':
36
+ return generatePrismaSeed(tableName, mockData);
37
+ case 'sql':
38
+ default:
39
+ return generateSQL(tableName, mockData, dialect);
40
+ }
41
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Formats mock data objects into pretty-printed JSON text.
3
+ * @param {Array<Record<string, any>>} mockData - Array of mock data row objects.
4
+ * @returns {string} JSON text.
5
+ */
6
+ export function generateJSON(mockData) {
7
+ if (!mockData) return '[]';
8
+
9
+ const cleanedData = mockData.map(row => {
10
+ const cleanRow = {};
11
+ Object.keys(row).forEach(key => {
12
+ let val = row[key];
13
+ if (val && typeof val === 'object') {
14
+ if (val.rawSql) {
15
+ const geomMatch = val.rawSql.match(/ST_GeomFromText\('([^']+)'\)/i);
16
+ val = geomMatch ? geomMatch[1] : val.rawSql;
17
+ } else if (val.rawHex) {
18
+ val = val.rawHex;
19
+ }
20
+ }
21
+ cleanRow[key] = val;
22
+ });
23
+ return cleanRow;
24
+ });
25
+
26
+ return JSON.stringify(cleanedData, null, 2);
27
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Formats mock data objects into a runnable Prisma Client seed script.
3
+ * @param {string} tableName - Target Prisma model / table name.
4
+ * @param {Array<Record<string, any>>} mockData - Array of mock data row objects.
5
+ * @returns {string} Runnable JavaScript Prisma seed script.
6
+ */
7
+ export function generatePrismaSeed(tableName, mockData) {
8
+ if (!mockData || mockData.length === 0) return '';
9
+
10
+ const modelName = tableName.charAt(0).toLowerCase() + tableName.slice(1);
11
+ const cleanedData = mockData.map(row => {
12
+ const cleanRow = {};
13
+ Object.keys(row).forEach(key => {
14
+ let val = row[key];
15
+ if (val && typeof val === 'object') {
16
+ if (val.rawSql) {
17
+ const geomMatch = val.rawSql.match(/ST_GeomFromText\('([^']+)'\)/i);
18
+ val = geomMatch ? geomMatch[1] : val.rawSql;
19
+ } else if (val.rawHex) {
20
+ val = val.rawHex;
21
+ }
22
+ }
23
+ cleanRow[key] = val;
24
+ });
25
+ return cleanRow;
26
+ });
27
+
28
+ const jsonPayload = JSON.stringify(cleanedData, null, 4);
29
+
30
+ return `import { PrismaClient } from '@prisma/client';
31
+
32
+ const prisma = new PrismaClient();
33
+
34
+ async function main() {
35
+ console.log('Seeding ${tableName} model...');
36
+ const result = await prisma.${modelName}.createMany({
37
+ data: ${jsonPayload},
38
+ skipDuplicates: true,
39
+ });
40
+ console.log(\`Successfully seeded \${result.count} rows into ${tableName}.\`);
41
+ }
42
+
43
+ main()
44
+ .catch((e) => {
45
+ console.error(e);
46
+ process.exit(1);
47
+ })
48
+ .finally(async () => {
49
+ await prisma.$disconnect();
50
+ });
51
+ `;
52
+ }
@@ -0,0 +1,49 @@
1
+ import { getDialect } from './dialects.js';
2
+
3
+ /**
4
+ * Generates an SQL INSERT statement from table name and array of mock data row objects.
5
+ * @param {string} tableName - Target SQL table name.
6
+ * @param {Array<Record<string, any>>} mockData - Array of generated mock rows.
7
+ * @param {string} [dialectName='postgres'] - Target SQL dialect (postgres, mysql, sqlite, mssql, oracle).
8
+ * @returns {string} Formatted SQL INSERT statement.
9
+ */
10
+ export function generateSQL(tableName, mockData, dialectName = 'postgres') {
11
+ if (!mockData || mockData.length === 0) return '';
12
+
13
+ const dialect = getDialect(dialectName);
14
+ const quotedTable = dialect.quoteIdentifier(tableName);
15
+ const rawColumns = Object.keys(mockData[0]);
16
+ const quotedColumns = rawColumns.map(col => dialect.quoteIdentifier(col)).join(', ');
17
+
18
+ const values = mockData.map(row => {
19
+ const rowValues = Object.values(row).map(val => {
20
+ if (val === null || val === undefined) return 'NULL';
21
+ if (typeof val === 'boolean') return dialect.formatBoolean(val);
22
+ if (typeof val === 'number') return val;
23
+
24
+ if (val && typeof val === 'object') {
25
+ if (val.rawSql) return val.rawSql;
26
+ if (val.rawHex) return dialect.formatBinary(val.rawHex);
27
+ }
28
+
29
+ if (Array.isArray(val)) {
30
+ return dialect.formatArray(val);
31
+ }
32
+
33
+ if (typeof val === 'object') {
34
+ const jsonStr = dialect.escapeString(JSON.stringify(val));
35
+ return `'${jsonStr}'`;
36
+ }
37
+
38
+ if (typeof val === 'string') {
39
+ const escaped = dialect.escapeString(val);
40
+ return `'${escaped}'`;
41
+ }
42
+
43
+ return `'${val}'`;
44
+ });
45
+ return `(${rowValues.join(', ')})`;
46
+ });
47
+
48
+ return `INSERT INTO ${quotedTable} (${quotedColumns})\nVALUES\n ${values.join(',\n ')};`;
49
+ }
@@ -0,0 +1,264 @@
1
+ import { faker } from '@faker-js/faker';
2
+
3
+ /**
4
+ * Generates a mock data row object for the given column definitions.
5
+ * @param {Array<{ name: string, type: string, fullLine?: string, isEnum?: boolean, enumValues?: string[] }>} columns
6
+ * @returns {Record<string, any>} Mock row data map
7
+ */
8
+ export function generateRow(columns) {
9
+ const row = {};
10
+
11
+ columns.forEach(col => {
12
+ const colName = col.name;
13
+ const colNameLower = colName.toLowerCase();
14
+ const rawType = (col.type || '').trim();
15
+ const typeLower = rawType.toLowerCase();
16
+ const fullLineLower = (col.fullLine || '').toLowerCase();
17
+
18
+ const isArray = rawType.endsWith('[]') || typeLower.includes('array');
19
+ const baseTypeLower = typeLower.replace(/\[\]/g, '').replace(/\?/g, '').replace(/array/g, '').trim();
20
+
21
+ const generateSingleValue = (tLower, nameLower, lineLower) => {
22
+ // 0. Explicit Schema Enums
23
+ if (col.isEnum && col.enumValues && col.enumValues.length > 0) {
24
+ return faker.helpers.arrayElement(col.enumValues);
25
+ }
26
+
27
+ // 1. UUID / CUID / Identifiers
28
+ if (
29
+ tLower.includes('uuid') ||
30
+ lineLower.includes('uuid()') ||
31
+ lineLower.includes('cuid()') ||
32
+ lineLower.includes('ulid()') ||
33
+ lineLower.includes('@db.uuid') ||
34
+ (nameLower.endsWith('id') && (tLower.includes('string') || tLower.includes('varchar') || tLower.includes('char') || tLower.includes('text')))
35
+ ) {
36
+ return faker.string.uuid();
37
+ }
38
+
39
+ // 2. Geometric / Spatial Types (OpenGIS ST_GeomFromText standard)
40
+ if (tLower.includes('point') || lineLower.includes('@db.point')) {
41
+ const lng = faker.location.longitude().toFixed(2);
42
+ const lat = faker.location.latitude().toFixed(2);
43
+ return { rawSql: `ST_GeomFromText('POINT(${lng} ${lat})')` };
44
+ }
45
+ if (tLower.includes('linestring') || tLower.includes('lseg') || lineLower.includes('@db.lseg')) {
46
+ return { rawSql: `ST_GeomFromText('LINESTRING(0 0, 30 48)')` };
47
+ }
48
+ if (tLower.includes('polygon') || lineLower.includes('@db.polygon')) {
49
+ return { rawSql: `ST_GeomFromText('POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))')` };
50
+ }
51
+ if (tLower === 'circle' || tLower === 'box' || tLower === 'path') {
52
+ return { rawSql: `ST_GeomFromText('POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))')` };
53
+ }
54
+
55
+ // 3. XML & Document Types
56
+ if (tLower === 'xml' || lineLower.includes('@db.xml')) {
57
+ return `<document><id>${faker.number.int()}</id><content>${faker.lorem.word()}</content></document>`;
58
+ }
59
+
60
+ // 4. Money / Currency Types
61
+ if (tLower.includes('money') || lineLower.includes('@db.money')) {
62
+ return parseFloat(faker.finance.amount({ min: 10, max: 5000, dec: 2 }));
63
+ }
64
+
65
+ // 5. Time Interval / Duration Types
66
+ if (tLower.includes('interval') || nameLower.includes('duration') || nameLower.includes('interval') || lineLower.includes('@db.interval')) {
67
+ return faker.helpers.arrayElement(['1 day', '2 hours 30 mins', '5 days 12 hours', '01:30:00']);
68
+ }
69
+
70
+ // 6. Inline SQL Enum Types
71
+ if (tLower.startsWith('enum')) {
72
+ const enumMatch = rawType.match(/enum\s*\(([^)]+)\)/i);
73
+ if (enumMatch) {
74
+ const values = enumMatch[1]
75
+ .split(',')
76
+ .map(v => v.trim().replace(/^['"]|['"]$/g, ''));
77
+ if (values.length > 0) return faker.helpers.arrayElement(values);
78
+ }
79
+ }
80
+ if (nameLower.includes('role')) return faker.helpers.arrayElement(['ADMIN', 'USER', 'GUEST']);
81
+ if (nameLower.includes('status') || nameLower.includes('state')) return faker.helpers.arrayElement(['ACTIVE', 'INACTIVE', 'PENDING', 'COMPLETED']);
82
+
83
+ // 7. Integer Types (Int, BigInt, SmallInt, TinyInt, Serial, etc.)
84
+ if (
85
+ tLower.includes('int') ||
86
+ tLower.includes('serial') ||
87
+ tLower.includes('number') ||
88
+ lineLower.includes('@db.smallint') ||
89
+ lineLower.includes('@db.tinyint') ||
90
+ (nameLower.endsWith('id') && !tLower.includes('string'))
91
+ ) {
92
+ if (tLower.includes('smallint') || tLower.includes('tinyint') || tLower.includes('int2') || tLower.includes('smallserial') || lineLower.includes('@db.smallint')) {
93
+ return faker.number.int({ min: 1, max: 100 });
94
+ }
95
+ if (nameLower.includes('age')) {
96
+ return faker.number.int({ min: 18, max: 80 });
97
+ }
98
+ if (nameLower.includes('year')) {
99
+ return faker.number.int({ min: 1995, max: 2026 });
100
+ }
101
+ if (nameLower.includes('count') || nameLower.includes('quantity') || nameLower.includes('stock')) {
102
+ return faker.number.int({ min: 0, max: 500 });
103
+ }
104
+ return faker.number.int({ min: 1, max: 10000 });
105
+ }
106
+
107
+ // 8. Float / Double / Decimal / Real / Numeric Types
108
+ if (
109
+ tLower.includes('float') ||
110
+ tLower.includes('decimal') ||
111
+ tLower.includes('double') ||
112
+ tLower.includes('real') ||
113
+ tLower.includes('numeric') ||
114
+ nameLower.includes('price') ||
115
+ nameLower.includes('amount') ||
116
+ nameLower.includes('cost') ||
117
+ nameLower.includes('balance') ||
118
+ nameLower.includes('salary') ||
119
+ nameLower.includes('rate') ||
120
+ nameLower.includes('tax') ||
121
+ nameLower.includes('cgpa') ||
122
+ nameLower.includes('gpa')
123
+ ) {
124
+ if (nameLower.includes('cgpa') || nameLower.includes('gpa')) {
125
+ return parseFloat(faker.number.float({ min: 2.0, max: 4.0, fractionDigits: 2 }).toFixed(2));
126
+ }
127
+ if (nameLower.includes('rate') || nameLower.includes('percent')) {
128
+ return parseFloat(faker.number.float({ min: 0, max: 1, fractionDigits: 2 }).toFixed(2));
129
+ }
130
+ return parseFloat(faker.finance.amount({ min: 10, max: 1000, dec: 2 }));
131
+ }
132
+
133
+ // 9. Boolean Types
134
+ if (
135
+ tLower.includes('bool') ||
136
+ tLower.includes('bit') ||
137
+ nameLower.startsWith('is_') ||
138
+ nameLower.startsWith('has_') ||
139
+ nameLower.startsWith('can_') ||
140
+ nameLower.startsWith('should_') ||
141
+ nameLower.startsWith('is') ||
142
+ nameLower.startsWith('has')
143
+ ) {
144
+ return faker.datatype.boolean();
145
+ }
146
+
147
+ // 10. Date & Time Types (Date, Time, Timetz, Timestamp, Timestamptz, DateTime)
148
+ if (tLower.includes('date') || tLower.includes('time') || tLower.includes('timestamp') || lineLower.includes('@db.date') || lineLower.includes('@db.time')) {
149
+ if (nameLower.includes('birth') || nameLower.includes('dob') || nameLower.includes('birthday')) {
150
+ return faker.date.birthdate().toISOString().slice(0, 10);
151
+ }
152
+ if (tLower === 'date' || lineLower.includes('@db.date')) {
153
+ return faker.date.past().toISOString().slice(0, 10);
154
+ }
155
+ if (tLower.includes('timetz') || tLower.includes('with time zone')) {
156
+ return faker.date.past().toISOString().slice(11, 19) + '+00';
157
+ }
158
+ if (tLower === 'time' || lineLower.includes('@db.time')) {
159
+ return faker.date.past().toISOString().slice(11, 19);
160
+ }
161
+ return faker.date.past().toISOString().slice(0, 19).replace('T', ' ');
162
+ }
163
+
164
+ // 11. JSON / Semi-Structured Types
165
+ if (tLower.includes('json')) {
166
+ return {
167
+ theme: faker.helpers.arrayElement(['dark', 'light']),
168
+ notifications: faker.datatype.boolean(),
169
+ views: faker.number.int({ min: 10, max: 500 })
170
+ };
171
+ }
172
+
173
+ // 12. Binary / Bytes / Blob Types
174
+ if (
175
+ tLower.includes('byte') ||
176
+ tLower.includes('blob') ||
177
+ tLower.includes('binary')
178
+ ) {
179
+ const hex = faker.string.hexadecimal({ length: 16, prefix: '' });
180
+ return { rawHex: hex };
181
+ }
182
+
183
+ // 13. Network / IP / MAC Types
184
+ if (tLower.includes('inet') || lineLower.includes('@db.inet') || tLower.includes('cidr')) {
185
+ return faker.internet.ip();
186
+ }
187
+ if (tLower.includes('macaddr') || lineLower.includes('@db.macaddr') || tLower.includes('mac')) {
188
+ return faker.internet.mac();
189
+ }
190
+
191
+ // 14. Strings & Character Types with Smart Field Heuristics
192
+ let strVal = '';
193
+ if (nameLower.includes('email') || nameLower.includes('mail')) {
194
+ strVal = faker.internet.email();
195
+ } else if (nameLower.includes('first_name') || nameLower.includes('firstname')) {
196
+ strVal = faker.person.firstName();
197
+ } else if (nameLower.includes('last_name') || nameLower.includes('lastname')) {
198
+ strVal = faker.person.lastName();
199
+ } else if (nameLower.includes('full_name') || nameLower.includes('fullname') || (nameLower.includes('name') && !nameLower.includes('file'))) {
200
+ strVal = faker.person.fullName();
201
+ } else if (nameLower.includes('user') || nameLower.includes('username') || nameLower.includes('handle')) {
202
+ strVal = faker.internet.username();
203
+ } else if (nameLower.includes('phone') || nameLower.includes('mobile') || nameLower.includes('tel') || nameLower.includes('cell')) {
204
+ strVal = faker.phone.number();
205
+ } else if (nameLower.includes('url') || nameLower.includes('website') || nameLower.includes('link') || nameLower.includes('domain')) {
206
+ strVal = faker.internet.url();
207
+ } else if (nameLower.includes('avatar') || nameLower.includes('image') || nameLower.includes('photo') || nameLower.includes('picture') || nameLower.includes('logo') || nameLower.includes('icon')) {
208
+ strVal = faker.image.avatar();
209
+ } else if (nameLower.includes('company') || nameLower.includes('org') || nameLower.includes('publisher')) {
210
+ strVal = faker.company.name();
211
+ } else if (nameLower.includes('job') || nameLower.includes('position') || nameLower.includes('occupation')) {
212
+ strVal = faker.person.jobTitle();
213
+ } else if (nameLower.includes('gender') || nameLower === 'sex') {
214
+ strVal = faker.person.sex();
215
+ } else if (nameLower.includes('department') || nameLower.includes('dept')) {
216
+ strVal = faker.commerce.department();
217
+ } else if (nameLower.includes('currency')) {
218
+ strVal = faker.finance.currencyCode();
219
+ } else if (nameLower.includes('birth') || nameLower.includes('dob') || nameLower.includes('birthday')) {
220
+ strVal = faker.date.birthdate().toISOString().slice(0, 10);
221
+ } else if (nameLower.includes('address') || nameLower.includes('street')) {
222
+ strVal = faker.location.streetAddress();
223
+ } else if (nameLower.includes('city')) {
224
+ strVal = faker.location.city();
225
+ } else if (nameLower.includes('country')) {
226
+ strVal = faker.location.country();
227
+ } else if (nameLower.includes('zip') || nameLower.includes('postal') || nameLower.includes('zipcode')) {
228
+ strVal = faker.location.zipCode();
229
+ } else if (nameLower.includes('bio') || nameLower.includes('description') || nameLower.includes('content') || nameLower.includes('body') || nameLower.includes('summary') || nameLower.includes('notes')) {
230
+ strVal = faker.lorem.paragraph();
231
+ } else if (nameLower.includes('title') || nameLower.includes('subject') || nameLower.includes('headline') || nameLower.includes('caption')) {
232
+ strVal = faker.lorem.sentence();
233
+ } else if (nameLower.includes('slug')) {
234
+ strVal = faker.lorem.slug();
235
+ } else {
236
+ strVal = faker.lorem.word();
237
+ }
238
+
239
+ // Truncate string if type specifies length VARCHAR(N) or CHAR(N)
240
+ const lengthMatch = rawType.match(/(?:varchar|char|nvarchar|nchar)\s*\(\s*(\d+)\s*\)/i);
241
+ if (lengthMatch && typeof strVal === 'string') {
242
+ const maxLen = parseInt(lengthMatch[1], 10);
243
+ if (maxLen > 0 && strVal.length > maxLen) {
244
+ strVal = strVal.slice(0, maxLen);
245
+ }
246
+ }
247
+
248
+ return strVal;
249
+ };
250
+
251
+ if (isArray) {
252
+ const count = faker.number.int({ min: 2, max: 4 });
253
+ const list = [];
254
+ for (let k = 0; k < count; k++) {
255
+ list.push(generateSingleValue(baseTypeLower, colNameLower, fullLineLower));
256
+ }
257
+ row[colName] = list;
258
+ } else {
259
+ row[colName] = generateSingleValue(typeLower, colNameLower, fullLineLower);
260
+ }
261
+ });
262
+
263
+ return row;
264
+ }
package/src/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { parseSchema, parseSqlSchema, parsePrismaSchema, getSchemaTableNames } from './parsers/index.js';
4
+ import { generateRow } from './generators/dataGenerator.js';
5
+ import { formatData, generateSQL, generateCSV, generateJSON, generatePrismaSeed } from './formatters/index.js';
6
+
7
+ export {
8
+ parseSchema,
9
+ parseSqlSchema,
10
+ parsePrismaSchema,
11
+ getSchemaTableNames,
12
+ generateRow,
13
+ formatData,
14
+ generateSQL,
15
+ generateCSV,
16
+ generateJSON,
17
+ generatePrismaSeed
18
+ };
19
+
20
+ /**
21
+ * Weaves mock data from a schema file and writes formatted output to a destination file.
22
+ * @param {string} file - Path to schema file (.sql or .prisma).
23
+ * @param {object} [options] - Options map.
24
+ * @param {number} [options.rows=10] - Number of rows to generate.
25
+ * @param {string} [options.output] - Target output file.
26
+ * @param {string} [options.format='sql'] - Target format (sql, csv, json, prisma).
27
+ * @param {string} [options.dialect='postgres'] - Target SQL dialect (postgres, mysql, sqlite, mssql, oracle).
28
+ * @param {string} [options.table] - Target table/model name to extract from multi-entity schema.
29
+ * @returns {{ tableName: string, rowCount: number, outputFile: string, content: string }}
30
+ */
31
+ export function weave(file, options = {}) {
32
+ const rowCount = parseInt(options.rows || 10, 10);
33
+ const format = (options.format || 'sql').toLowerCase().trim();
34
+ const dialect = (options.dialect || 'postgres').toLowerCase().trim();
35
+ const targetTable = options.table || options.targetTable;
36
+
37
+ let defaultExt = '.sql';
38
+ if (format === 'csv') defaultExt = '.csv';
39
+ else if (format === 'json') defaultExt = '.json';
40
+ else if (format === 'prisma' || format === 'prismaseed') defaultExt = '.js';
41
+
42
+ let outputFile = options.output;
43
+ if (!outputFile) {
44
+ outputFile = `seed${defaultExt}`;
45
+ } else if (!path.extname(outputFile)) {
46
+ outputFile = `${outputFile}${defaultExt}`;
47
+ }
48
+
49
+ const schemaText = fs.readFileSync(file, 'utf-8');
50
+ const isPrisma = file.endsWith('.prisma');
51
+
52
+ const parsedData = parseSchema(schemaText, isPrisma, targetTable);
53
+
54
+ const mockData = [];
55
+ for (let i = 0; i < rowCount; i++) {
56
+ mockData.push(generateRow(parsedData.columns));
57
+ }
58
+
59
+ const finalContent = formatData(parsedData.tableName, mockData, { format, dialect });
60
+ fs.writeFileSync(outputFile, finalContent);
61
+
62
+ return {
63
+ tableName: parsedData.tableName,
64
+ rowCount,
65
+ outputFile,
66
+ content: finalContent
67
+ };
68
+ }
@@ -0,0 +1,31 @@
1
+ import { parseSqlSchema, getSqlTableNames } from './sqlParser.js';
2
+ import { parsePrismaSchema, getPrismaModelNames } from './prismaParser.js';
3
+
4
+ export { parseSqlSchema, parsePrismaSchema, getSqlTableNames, getPrismaModelNames };
5
+
6
+ /**
7
+ * Gets all entity (table or model) names from a schema text.
8
+ * @param {string} text - Raw schema text.
9
+ * @param {boolean} isPrisma - True if Prisma schema.
10
+ * @returns {string[]} Array of table / model names.
11
+ */
12
+ export function getSchemaTableNames(text, isPrisma) {
13
+ return isPrisma ? getPrismaModelNames(text) : getSqlTableNames(text);
14
+ }
15
+
16
+ /**
17
+ * Parses a database schema (SQL or Prisma) into a normalized representation.
18
+ * @param {string} text - The raw schema text.
19
+ * @param {boolean} isPrisma - True if the schema is a Prisma file, false for SQL.
20
+ * @param {string} [targetTable] - Optional table/model name to target.
21
+ * @returns {{ tableName: string, columns: Array<{ name: string, type: string, isAutoIncrement?: boolean }> }}
22
+ */
23
+ export function parseSchema(text, isPrisma, targetTable) {
24
+ const parsed = isPrisma ? parsePrismaSchema(text, targetTable) : parseSqlSchema(text, targetTable);
25
+ // Omit auto-incrementing columns so the database handles primary key generation naturally
26
+ const filteredColumns = parsed.columns.filter(col => !col.isAutoIncrement);
27
+ return {
28
+ tableName: parsed.tableName,
29
+ columns: filteredColumns.length > 0 ? filteredColumns : parsed.columns
30
+ };
31
+ }
@@ -0,0 +1,73 @@
1
+ // --- HELPER TO STRIP PRISMA COMMENTS ---
2
+ export function stripPrismaComments(text) {
3
+ let clean = text.replace(/\/\*[\s\S]*?\*\//g, '');
4
+ clean = clean.replace(/\/\/.*/g, '');
5
+ return clean;
6
+ }
7
+
8
+ // --- HELPER TO GET ALL MODEL NAMES FROM PRISMA SCHEMA ---
9
+ export function getPrismaModelNames(text) {
10
+ const cleanText = stripPrismaComments(text);
11
+ const matches = cleanText.matchAll(/model\s+(\w+)\s*{/gi);
12
+ const names = [];
13
+ for (const match of matches) {
14
+ names.push(match[1]);
15
+ }
16
+ return names;
17
+ }
18
+
19
+ // --- PRISMA SCHEMA PARSER ---
20
+ export function parsePrismaSchema(text, targetModel) {
21
+ const cleanText = stripPrismaComments(text);
22
+ const allModelNames = getPrismaModelNames(text);
23
+
24
+ const enums = {};
25
+ const enumMatches = cleanText.matchAll(/enum\s+(\w+)\s*{([\s\S]+?)}/gi);
26
+ for (const match of enumMatches) {
27
+ const enumName = match[1];
28
+ const values = match[2]
29
+ .split('\n')
30
+ .map(v => v.trim())
31
+ .filter(v => v && !v.startsWith('//'));
32
+ enums[enumName] = values;
33
+ }
34
+
35
+ const modelMatches = [...cleanText.matchAll(/model\s+(\w+)\s*{([\s\S]+?)}/gi)];
36
+ if (modelMatches.length === 0) throw new Error("Could not find a valid Prisma model.");
37
+
38
+ let selectedMatch = modelMatches[0];
39
+ if (targetModel) {
40
+ const targetLower = targetModel.toLowerCase().trim();
41
+ const found = modelMatches.find(m => m[1].toLowerCase() === targetLower);
42
+ if (found) {
43
+ selectedMatch = found;
44
+ } else {
45
+ throw new Error(`Prisma model '${targetModel}' not found in schema.`);
46
+ }
47
+ }
48
+
49
+ const tableName = selectedMatch[1];
50
+ const columns = selectedMatch[2].trim().split('\n').map(line => {
51
+ const trimmed = line.trim();
52
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) return null;
53
+ if (trimmed.includes('@relation')) return null;
54
+
55
+ const parts = trimmed.split(/\s+/);
56
+ if (parts.length < 2) return null;
57
+
58
+ const name = parts[0];
59
+ const rawType = parts[1];
60
+ const cleanType = rawType.replace(/\?$/, '').replace(/\[\]$/, '');
61
+
62
+ // Skip implicit and explicit Prisma relation fields
63
+ if (allModelNames.includes(cleanType)) return null;
64
+
65
+ const isEnum = Boolean(enums[cleanType]);
66
+ const enumValues = enums[cleanType] || null;
67
+ const isAutoIncrement = trimmed.includes('@default(autoincrement())');
68
+
69
+ return { name, type: rawType, fullLine: trimmed, isEnum, enumValues, isAutoIncrement };
70
+ }).filter(Boolean);
71
+
72
+ return { tableName, columns };
73
+ }
@@ -0,0 +1,129 @@
1
+ // --- HELPER TO STRIP SQL COMMENTS ---
2
+ export function stripSqlComments(text) {
3
+ let clean = text.replace(/\/\*[\s\S]*?\*\//g, '');
4
+ clean = clean.replace(/--.*$/gm, '');
5
+ return clean;
6
+ }
7
+
8
+ // --- HELPER TO GET ALL TABLE NAMES FROM SQL SCHEMA ---
9
+ export function getSqlTableNames(text) {
10
+ const cleanText = stripSqlComments(text);
11
+ const matches = cleanText.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:[`"]?[\w]+[`"]?\.)?[`"]?[\w]+[`"]?|\"[^\"]+\"|`[^`]+`)\s*\(/gi);
12
+ const names = [];
13
+ for (const match of matches) {
14
+ const rawName = match[1].replace(/[`"]/g, '');
15
+ names.push(rawName);
16
+ }
17
+ return names;
18
+ }
19
+
20
+ // --- SQL COLUMN PARSER ---
21
+ export function parseSqlColumnLine(line) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed) return null;
24
+
25
+ const match = trimmed.match(/^([`"]?[\w.]+\b[`"]?)\s+(.+)$/i);
26
+ if (!match) return null;
27
+
28
+ const rawName = match[1].replace(/[`"]/g, '');
29
+ const remainder = match[2];
30
+
31
+ const upperName = rawName.toUpperCase();
32
+ if (['PRIMARY', 'FOREIGN', 'CONSTRAINT', 'UNIQUE', 'INDEX', 'KEY', 'CHECK', 'FULLTEXT', 'SPATIAL'].includes(upperName)) {
33
+ return null;
34
+ }
35
+
36
+ const constraintKeywords = ['NOT', 'NULL', 'PRIMARY', 'DEFAULT', 'UNIQUE', 'REFERENCES', 'AUTO_INCREMENT', 'GENERATED', 'IDENTITY', 'CHECK', 'COMMENT'];
37
+
38
+ let typeStr = '';
39
+ let parenDepth = 0;
40
+ let i = 0;
41
+ while (i < remainder.length) {
42
+ const char = remainder[i];
43
+ if (char === '(') parenDepth++;
44
+ else if (char === ')') parenDepth--;
45
+
46
+ if (parenDepth === 0) {
47
+ const rest = remainder.slice(i).trim();
48
+ const nextWord = rest.split(/\s+/)[0].toUpperCase();
49
+ if (typeStr.trim().length > 0 && constraintKeywords.includes(nextWord)) {
50
+ break;
51
+ }
52
+ }
53
+ typeStr += char;
54
+ i++;
55
+ }
56
+
57
+ const lineLower = trimmed.toLowerCase();
58
+ const typeLower = typeStr.trim().toLowerCase();
59
+ const isAutoIncrement =
60
+ lineLower.includes('auto_increment') ||
61
+ lineLower.includes('autoincrement') ||
62
+ lineLower.includes('identity') ||
63
+ lineLower.includes('nextval(') ||
64
+ lineLower.includes('generated') ||
65
+ typeLower === 'serial' ||
66
+ typeLower === 'bigserial' ||
67
+ typeLower === 'smallserial';
68
+
69
+ return { name: rawName, type: typeStr.trim(), isAutoIncrement };
70
+ }
71
+
72
+ // --- MAIN SQL SCHEMA PARSER ---
73
+ export function parseSqlSchema(text, targetTable) {
74
+ const cleanText = stripSqlComments(text);
75
+ const tableMatches = [...cleanText.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:[`"]?[\w]+[`"]?\.)?[`"]?[\w]+[`"]?|\"[^\"]+\"|`[^`]+`)\s*\(([\s\S]+?)\)(?:;|\s*$|\s*(?=CREATE))/gi)];
76
+
77
+ if (tableMatches.length === 0) {
78
+ throw new Error("Could not find a valid CREATE TABLE statement.");
79
+ }
80
+
81
+ let selectedMatch = tableMatches[0];
82
+ if (targetTable) {
83
+ const targetLower = targetTable.toLowerCase().trim();
84
+ const found = tableMatches.find(m => m[1].replace(/[`"]/g, '').toLowerCase() === targetLower);
85
+ if (found) {
86
+ selectedMatch = found;
87
+ } else {
88
+ throw new Error(`Table '${targetTable}' not found in schema.`);
89
+ }
90
+ }
91
+
92
+ const tableName = selectedMatch[1].replace(/[`"]/g, '');
93
+ const body = selectedMatch[2];
94
+
95
+ const columnDefs = [];
96
+ let current = '';
97
+ let parenDepth = 0;
98
+ let inString = false;
99
+ let quoteChar = '';
100
+
101
+ for (let i = 0; i < body.length; i++) {
102
+ const char = body[i];
103
+
104
+ if ((char === "'" || char === '"' || char === '`') && (i === 0 || body[i - 1] !== '\\')) {
105
+ if (!inString) {
106
+ inString = true;
107
+ quoteChar = char;
108
+ } else if (quoteChar === char) {
109
+ inString = false;
110
+ }
111
+ }
112
+
113
+ if (!inString) {
114
+ if (char === '(') parenDepth++;
115
+ else if (char === ')') parenDepth--;
116
+ }
117
+
118
+ if (char === ',' && parenDepth === 0 && !inString) {
119
+ if (current.trim()) columnDefs.push(current.trim());
120
+ current = '';
121
+ } else {
122
+ current += char;
123
+ }
124
+ }
125
+ if (current.trim()) columnDefs.push(current.trim());
126
+
127
+ const columns = columnDefs.map(parseSqlColumnLine).filter(Boolean);
128
+ return { tableName, columns };
129
+ }