@xubylele/schema-forge 1.5.1 → 1.5.2
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/cli.js +126 -9
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ function parseSchema(source) {
|
|
|
45
45
|
"timestamptz",
|
|
46
46
|
"date"
|
|
47
47
|
]);
|
|
48
|
+
const validIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
48
49
|
function normalizeColumnType3(type) {
|
|
49
50
|
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
50
51
|
}
|
|
@@ -53,12 +54,103 @@ function parseSchema(source) {
|
|
|
53
54
|
if (validBaseColumnTypes.has(normalizedType)) {
|
|
54
55
|
return true;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
const varcharMatch = normalizedType.match(/^varchar\((\d+)\)$/);
|
|
58
|
+
if (varcharMatch) {
|
|
59
|
+
const length = Number(varcharMatch[1]);
|
|
60
|
+
return Number.isInteger(length) && length > 0;
|
|
61
|
+
}
|
|
62
|
+
const numericMatch = normalizedType.match(/^numeric\((\d+),(\d+)\)$/);
|
|
63
|
+
if (numericMatch) {
|
|
64
|
+
const precision = Number(numericMatch[1]);
|
|
65
|
+
const scale = Number(numericMatch[2]);
|
|
66
|
+
return Number.isInteger(precision) && Number.isInteger(scale) && precision > 0 && scale >= 0 && scale <= precision;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function validateIdentifier(identifier, lineNum, context) {
|
|
71
|
+
if (!validIdentifierPattern.test(identifier)) {
|
|
72
|
+
throw new Error(`Line ${lineNum}: Invalid ${context} name '${identifier}'. Use letters, numbers, and underscores, and do not start with a number.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function validateDefaultValue(value, lineNum) {
|
|
76
|
+
let parenBalance = 0;
|
|
77
|
+
let inSingleQuote = false;
|
|
78
|
+
let inDoubleQuote = false;
|
|
79
|
+
for (let index = 0; index < value.length; index++) {
|
|
80
|
+
const char = value[index];
|
|
81
|
+
if (char === "'" && !inDoubleQuote) {
|
|
82
|
+
if (inSingleQuote && value[index + 1] === "'") {
|
|
83
|
+
index++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
inSingleQuote = !inSingleQuote;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (char === '"' && !inSingleQuote) {
|
|
90
|
+
if (inDoubleQuote && value[index + 1] === '"') {
|
|
91
|
+
index++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
inDoubleQuote = !inDoubleQuote;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "(") {
|
|
101
|
+
parenBalance++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === ")") {
|
|
105
|
+
parenBalance--;
|
|
106
|
+
if (parenBalance < 0) {
|
|
107
|
+
throw new Error(`Line ${lineNum}: Invalid default value '${value}'. Unmatched parentheses in function call.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
112
|
+
throw new Error(`Line ${lineNum}: Invalid default value '${value}'. Unterminated quoted string.`);
|
|
113
|
+
}
|
|
114
|
+
if (parenBalance > 0) {
|
|
115
|
+
if (parenBalance === 1) {
|
|
116
|
+
throw new Error(`Line ${lineNum}: Invalid default value '${value}'. Function call is missing closing parenthesis.`);
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Line ${lineNum}: Invalid default value '${value}'. Unmatched parentheses in function call.`);
|
|
119
|
+
}
|
|
57
120
|
}
|
|
58
121
|
function cleanLine(line) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
122
|
+
let inSingleQuote = false;
|
|
123
|
+
let inDoubleQuote = false;
|
|
124
|
+
for (let index = 0; index < line.length; index++) {
|
|
125
|
+
const char = line[index];
|
|
126
|
+
const nextChar = line[index + 1];
|
|
127
|
+
if (char === "'" && !inDoubleQuote) {
|
|
128
|
+
if (inSingleQuote && nextChar === "'") {
|
|
129
|
+
index++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
inSingleQuote = !inSingleQuote;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === '"' && !inSingleQuote) {
|
|
136
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
137
|
+
index++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
inDoubleQuote = !inDoubleQuote;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (char === "#") {
|
|
147
|
+
line = line.substring(0, index);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (char === "/" && nextChar === "/") {
|
|
151
|
+
line = line.substring(0, index);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
62
154
|
}
|
|
63
155
|
return line.trim();
|
|
64
156
|
}
|
|
@@ -67,6 +159,8 @@ function parseSchema(source) {
|
|
|
67
159
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
68
160
|
throw new Error(`Line ${lineNum}: Invalid foreign key format '${fkRef}'. Expected format: table.column`);
|
|
69
161
|
}
|
|
162
|
+
validateIdentifier(parts[0], lineNum, "foreign key table");
|
|
163
|
+
validateIdentifier(parts[1], lineNum, "foreign key column");
|
|
70
164
|
return {
|
|
71
165
|
table: parts[0],
|
|
72
166
|
column: parts[1]
|
|
@@ -75,11 +169,13 @@ function parseSchema(source) {
|
|
|
75
169
|
function parseColumn(line, lineNum) {
|
|
76
170
|
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
77
171
|
const modifiers = /* @__PURE__ */ new Set(["pk", "unique", "nullable", "default", "fk"]);
|
|
172
|
+
const appliedModifiers = /* @__PURE__ */ new Set();
|
|
78
173
|
if (tokens.length < 2) {
|
|
79
174
|
throw new Error(`Line ${lineNum}: Invalid column definition. Expected: <name> <type> [modifiers...]`);
|
|
80
175
|
}
|
|
81
176
|
const colName = tokens[0];
|
|
82
177
|
const colType = normalizeColumnType3(tokens[1]);
|
|
178
|
+
validateIdentifier(colName, lineNum, "column");
|
|
83
179
|
if (!isValidColumnType2(colType)) {
|
|
84
180
|
throw new Error(`Line ${lineNum}: Invalid column type '${tokens[1]}'. Valid types: ${Array.from(validBaseColumnTypes).join(", ")}, varchar(n), numeric(p,s)`);
|
|
85
181
|
}
|
|
@@ -91,16 +187,28 @@ function parseSchema(source) {
|
|
|
91
187
|
let i = 2;
|
|
92
188
|
while (i < tokens.length) {
|
|
93
189
|
const modifier = tokens[i];
|
|
190
|
+
const markModifierApplied = (name) => {
|
|
191
|
+
if (appliedModifiers.has(name)) {
|
|
192
|
+
throw new Error(`Line ${lineNum}: Duplicate modifier '${name}'`);
|
|
193
|
+
}
|
|
194
|
+
appliedModifiers.add(name);
|
|
195
|
+
};
|
|
94
196
|
switch (modifier) {
|
|
95
197
|
case "pk":
|
|
198
|
+
markModifierApplied("pk");
|
|
96
199
|
column.primaryKey = true;
|
|
97
200
|
i++;
|
|
98
201
|
break;
|
|
99
202
|
case "unique":
|
|
203
|
+
markModifierApplied("unique");
|
|
100
204
|
column.unique = true;
|
|
101
205
|
i++;
|
|
102
206
|
break;
|
|
103
207
|
case "nullable":
|
|
208
|
+
if (appliedModifiers.has("not null")) {
|
|
209
|
+
throw new Error(`Line ${lineNum}: Cannot combine 'nullable' with 'not null'`);
|
|
210
|
+
}
|
|
211
|
+
markModifierApplied("nullable");
|
|
104
212
|
column.nullable = true;
|
|
105
213
|
i++;
|
|
106
214
|
break;
|
|
@@ -108,27 +216,35 @@ function parseSchema(source) {
|
|
|
108
216
|
if (tokens[i + 1] !== "null") {
|
|
109
217
|
throw new Error(`Line ${lineNum}: Unknown modifier 'not'`);
|
|
110
218
|
}
|
|
219
|
+
if (appliedModifiers.has("nullable")) {
|
|
220
|
+
throw new Error(`Line ${lineNum}: Cannot combine 'not null' with 'nullable'`);
|
|
221
|
+
}
|
|
222
|
+
markModifierApplied("not null");
|
|
111
223
|
column.nullable = false;
|
|
112
224
|
i += 2;
|
|
113
225
|
break;
|
|
114
226
|
case "default":
|
|
227
|
+
markModifierApplied("default");
|
|
115
228
|
i++;
|
|
116
229
|
if (i >= tokens.length) {
|
|
117
230
|
throw new Error(`Line ${lineNum}: 'default' modifier requires a value`);
|
|
118
231
|
}
|
|
119
232
|
{
|
|
120
233
|
const defaultTokens = [];
|
|
121
|
-
while (i < tokens.length && !modifiers.has(tokens[i])) {
|
|
234
|
+
while (i < tokens.length && !modifiers.has(tokens[i]) && !(tokens[i] === "not" && tokens[i + 1] === "null")) {
|
|
122
235
|
defaultTokens.push(tokens[i]);
|
|
123
236
|
i++;
|
|
124
237
|
}
|
|
125
238
|
if (defaultTokens.length === 0) {
|
|
126
239
|
throw new Error(`Line ${lineNum}: 'default' modifier requires a value`);
|
|
127
240
|
}
|
|
128
|
-
|
|
241
|
+
const defaultValue = defaultTokens.join(" ");
|
|
242
|
+
validateDefaultValue(defaultValue, lineNum);
|
|
243
|
+
column.default = defaultValue;
|
|
129
244
|
}
|
|
130
245
|
break;
|
|
131
246
|
case "fk":
|
|
247
|
+
markModifierApplied("fk");
|
|
132
248
|
i++;
|
|
133
249
|
if (i >= tokens.length) {
|
|
134
250
|
throw new Error(`Line ${lineNum}: 'fk' modifier requires a table.column reference`);
|
|
@@ -144,11 +260,12 @@ function parseSchema(source) {
|
|
|
144
260
|
}
|
|
145
261
|
function parseTableBlock(startLine) {
|
|
146
262
|
const firstLine = cleanLine(lines[startLine]);
|
|
147
|
-
const match = firstLine.match(/^table\s+(\w+)\s*\{
|
|
263
|
+
const match = firstLine.match(/^table\s+(\w+)\s*\{\s*$/);
|
|
148
264
|
if (!match) {
|
|
149
265
|
throw new Error(`Line ${startLine + 1}: Invalid table definition. Expected: table <name> {`);
|
|
150
266
|
}
|
|
151
267
|
const tableName = match[1];
|
|
268
|
+
validateIdentifier(tableName, startLine + 1, "table");
|
|
152
269
|
if (tables[tableName]) {
|
|
153
270
|
throw new Error(`Line ${startLine + 1}: Duplicate table definition '${tableName}'`);
|
|
154
271
|
}
|
|
@@ -2050,7 +2167,7 @@ var import_commander6 = require("commander");
|
|
|
2050
2167
|
// package.json
|
|
2051
2168
|
var package_default = {
|
|
2052
2169
|
name: "@xubylele/schema-forge",
|
|
2053
|
-
version: "1.5.
|
|
2170
|
+
version: "1.5.2",
|
|
2054
2171
|
description: "Universal migration generator from schema DSL",
|
|
2055
2172
|
main: "dist/cli.js",
|
|
2056
2173
|
type: "commonjs",
|
|
@@ -2097,7 +2214,7 @@ var package_default = {
|
|
|
2097
2214
|
devDependencies: {
|
|
2098
2215
|
"@changesets/cli": "^2.29.8",
|
|
2099
2216
|
"@types/node": "^25.2.3",
|
|
2100
|
-
"@xubylele/schema-forge-core": "^1.0
|
|
2217
|
+
"@xubylele/schema-forge-core": "^1.1.0",
|
|
2101
2218
|
"ts-node": "^10.9.2",
|
|
2102
2219
|
tsup: "^8.5.1",
|
|
2103
2220
|
typescript: "^5.9.3",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xubylele/schema-forge",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Universal migration generator from schema DSL",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@changesets/cli": "^2.29.8",
|
|
49
49
|
"@types/node": "^25.2.3",
|
|
50
|
-
"@xubylele/schema-forge-core": "^1.0
|
|
50
|
+
"@xubylele/schema-forge-core": "^1.1.0",
|
|
51
51
|
"ts-node": "^10.9.2",
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"typescript": "^5.9.3",
|