@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.
Files changed (2) hide show
  1. package/dist/cli.js +126 -9
  2. 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
- return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
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
- const commentIndex = line.search(/(?:\/\/|#)/);
60
- if (commentIndex !== -1) {
61
- line = line.substring(0, commentIndex);
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
- column.default = defaultTokens.join(" ");
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*\{?\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.1",
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.5",
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.1",
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.5",
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",