@schemashift/core 0.12.0 → 0.13.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/index.js CHANGED
@@ -136,10 +136,30 @@ var SchemaAnalyzer = class {
136
136
  // src/approval.ts
137
137
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
138
138
  import { join } from "path";
139
+
140
+ // src/constants.ts
141
+ var SCHEMASHIFT_DIR = ".schemashift";
142
+ var BACKUP_DIR = ".schemashift-backup";
143
+ var CONFIG_FILE_NAMES = [
144
+ ".schemashiftrc",
145
+ ".schemashiftrc.json",
146
+ ".schemashiftrc.yaml",
147
+ ".schemashiftrc.yml",
148
+ ".schemashiftrc.js",
149
+ ".schemashiftrc.cjs"
150
+ ];
151
+ var DEFAULT_CONFIG_FILE = ".schemashiftrc.json";
152
+ var INCREMENTAL_STATE_FILE = "incremental.json";
153
+ var AUDIT_LOG_FILE = "audit-log.json";
154
+ var SCHEMA_SNAPSHOT_FILE = "schema-snapshot.json";
155
+ var PENDING_DIR = "pending";
156
+ var TESTS_DIR = "tests";
157
+
158
+ // src/approval.ts
139
159
  var ApprovalManager = class {
140
160
  pendingDir;
141
161
  constructor(projectPath) {
142
- this.pendingDir = join(projectPath, ".schemashift", "pending");
162
+ this.pendingDir = join(projectPath, SCHEMASHIFT_DIR, PENDING_DIR);
143
163
  }
144
164
  /**
145
165
  * Create a new migration request for review.
@@ -188,8 +208,16 @@ var ApprovalManager = class {
188
208
  if (!existsSync(filePath)) {
189
209
  return null;
190
210
  }
191
- const content = readFileSync(filePath, "utf-8");
192
- return JSON.parse(content);
211
+ try {
212
+ const content = readFileSync(filePath, "utf-8");
213
+ const parsed = JSON.parse(content);
214
+ if (!this.isValidRequest(parsed)) {
215
+ return null;
216
+ }
217
+ return parsed;
218
+ } catch {
219
+ return null;
220
+ }
193
221
  }
194
222
  /**
195
223
  * List all migration requests, optionally filtered by status.
@@ -201,10 +229,14 @@ var ApprovalManager = class {
201
229
  const files = readdirSync(this.pendingDir).filter((f) => f.endsWith(".json"));
202
230
  const requests = [];
203
231
  for (const file of files) {
204
- const content = readFileSync(join(this.pendingDir, file), "utf-8");
205
- const request = JSON.parse(content);
206
- if (!status || request.status === status) {
207
- requests.push(request);
232
+ try {
233
+ const content = readFileSync(join(this.pendingDir, file), "utf-8");
234
+ const parsed = JSON.parse(content);
235
+ if (!this.isValidRequest(parsed)) continue;
236
+ if (!status || parsed.status === status) {
237
+ requests.push(parsed);
238
+ }
239
+ } catch {
208
240
  }
209
241
  }
210
242
  return requests.sort(
@@ -230,6 +262,11 @@ var ApprovalManager = class {
230
262
  const request = this.getRequest(requestId);
231
263
  return request?.status === "approved";
232
264
  }
265
+ isValidRequest(data) {
266
+ if (typeof data !== "object" || data === null) return false;
267
+ const obj = data;
268
+ return typeof obj.id === "string" && typeof obj.from === "string" && typeof obj.to === "string" && Array.isArray(obj.files) && typeof obj.requestedBy === "string" && typeof obj.status === "string" && ["pending", "approved", "rejected"].includes(obj.status);
269
+ }
233
270
  ensureDir() {
234
271
  if (!existsSync(this.pendingDir)) {
235
272
  mkdirSync(this.pendingDir, { recursive: true });
@@ -375,15 +412,13 @@ function transformMethodChain(chain, newBase, factoryMapper, methodMapper) {
375
412
  import { createHash } from "crypto";
376
413
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
377
414
  import { join as join2 } from "path";
378
- var AUDIT_DIR = ".schemashift";
379
- var AUDIT_FILE = "audit-log.json";
380
415
  var AUDIT_VERSION = 1;
381
416
  var MigrationAuditLog = class {
382
417
  logDir;
383
418
  logPath;
384
419
  constructor(projectPath) {
385
- this.logDir = join2(projectPath, AUDIT_DIR);
386
- this.logPath = join2(this.logDir, AUDIT_FILE);
420
+ this.logDir = join2(projectPath, SCHEMASHIFT_DIR);
421
+ this.logPath = join2(this.logDir, AUDIT_LOG_FILE);
387
422
  }
388
423
  /**
389
424
  * Append a new entry to the audit log.
@@ -427,7 +462,11 @@ var MigrationAuditLog = class {
427
462
  if (!content.trim()) {
428
463
  return { version: AUDIT_VERSION, entries: [] };
429
464
  }
430
- return JSON.parse(content);
465
+ const parsed = JSON.parse(content);
466
+ if (!this.isValidAuditLog(parsed)) {
467
+ return { version: AUDIT_VERSION, entries: [] };
468
+ }
469
+ return parsed;
431
470
  } catch {
432
471
  return { version: AUDIT_VERSION, entries: [] };
433
472
  }
@@ -597,6 +636,13 @@ var MigrationAuditLog = class {
597
636
  gitCommit: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || void 0
598
637
  };
599
638
  }
639
+ isValidAuditLog(data) {
640
+ if (typeof data !== "object" || data === null) return false;
641
+ const obj = data;
642
+ if (typeof obj.version !== "number") return false;
643
+ if (!Array.isArray(obj.entries)) return false;
644
+ return true;
645
+ }
600
646
  write(log) {
601
647
  if (!existsSync2(this.logDir)) {
602
648
  mkdirSync2(this.logDir, { recursive: true });
@@ -1040,6 +1086,12 @@ import { join as join4 } from "path";
1040
1086
  // src/ecosystem.ts
1041
1087
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1042
1088
  import { join as join3 } from "path";
1089
+ function parseMajorVersion(version) {
1090
+ const match = version.match(/(\d+)/);
1091
+ const num = match?.[1] ? Number.parseInt(match[1], 10) : 0;
1092
+ if (!Number.isFinite(num) || num < 0 || num > 999) return 0;
1093
+ return num;
1094
+ }
1043
1095
  var ECOSYSTEM_RULES = [
1044
1096
  // ORM integrations
1045
1097
  {
@@ -1081,8 +1133,7 @@ var ECOSYSTEM_RULES = [
1081
1133
  category: "api",
1082
1134
  migrations: ["zod-v3->v4"],
1083
1135
  check: (version) => {
1084
- const majorMatch = version.match(/(\d+)/);
1085
- const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
1136
+ const major = parseMajorVersion(version);
1086
1137
  if (major < 11) {
1087
1138
  return {
1088
1139
  issue: `tRPC v${major} expects Zod v3 types. A v3 ZodType is not assignable to a v4 ZodType.`,
@@ -1115,8 +1166,7 @@ var ECOSYSTEM_RULES = [
1115
1166
  category: "validation-util",
1116
1167
  migrations: ["zod-v3->v4"],
1117
1168
  check: (version) => {
1118
- const majorMatch = version.match(/(\d+)/);
1119
- const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
1169
+ const major = parseMajorVersion(version);
1120
1170
  if (major < 4) {
1121
1171
  return {
1122
1172
  issue: `zod-validation-error v${major} is not compatible with Zod v4.`,
@@ -1406,8 +1456,7 @@ var ECOSYSTEM_RULES = [
1406
1456
  category: "validation-util",
1407
1457
  migrations: ["zod-v3->v4"],
1408
1458
  check: (version) => {
1409
- const majorMatch = version.match(/(\d+)/);
1410
- const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
1459
+ const major = parseMajorVersion(version);
1411
1460
  if (major < 4) {
1412
1461
  return {
1413
1462
  issue: "zod-to-json-schema v3 may not fully support Zod v4 schemas.",
@@ -1802,7 +1851,7 @@ async function loadConfig(configPath) {
1802
1851
  include: ["**/*.ts", "**/*.tsx"],
1803
1852
  exclude: ["**/node_modules/**", "**/dist/**", "**/*.d.ts"],
1804
1853
  git: { enabled: false },
1805
- backup: { enabled: true, dir: ".schemashift-backup" },
1854
+ backup: { enabled: true, dir: BACKUP_DIR },
1806
1855
  ...result?.config
1807
1856
  };
1808
1857
  }
@@ -1913,6 +1962,75 @@ function suggestCrossFieldPattern(whenCode) {
1913
1962
  return null;
1914
1963
  }
1915
1964
 
1965
+ // src/dead-schema-detector.ts
1966
+ var DeadSchemaDetector = class {
1967
+ detect(sourceFiles) {
1968
+ const schemas = this.collectSchemaDefinitions(sourceFiles);
1969
+ const unusedSchemas = this.findUnusedSchemas(schemas, sourceFiles);
1970
+ return {
1971
+ unusedSchemas,
1972
+ totalSchemas: schemas.length,
1973
+ summary: unusedSchemas.length > 0 ? `Found ${unusedSchemas.length} unused schema(s) out of ${schemas.length} total that may be safely removed.` : `All ${schemas.length} schema(s) are referenced.`
1974
+ };
1975
+ }
1976
+ collectSchemaDefinitions(sourceFiles) {
1977
+ const schemas = [];
1978
+ const schemaPattern = /(?:const|let|var|export\s+(?:const|let|var))\s+(\w+)\s*=\s*(?:z\.|yup\.|Yup\.|Joi\.|t\.|v\.|type\(|object\(|string\(|S\.)/;
1979
+ for (const file of sourceFiles) {
1980
+ const text = file.getFullText();
1981
+ const lines = text.split("\n");
1982
+ const filePath = file.getFilePath();
1983
+ for (let i = 0; i < lines.length; i++) {
1984
+ const line = lines[i];
1985
+ if (!line) continue;
1986
+ const match = schemaPattern.exec(line);
1987
+ if (match?.[1]) {
1988
+ schemas.push({
1989
+ schemaName: match[1],
1990
+ filePath,
1991
+ lineNumber: i + 1
1992
+ });
1993
+ }
1994
+ }
1995
+ }
1996
+ return schemas;
1997
+ }
1998
+ findUnusedSchemas(schemas, sourceFiles) {
1999
+ const fileContents = /* @__PURE__ */ new Map();
2000
+ for (const file of sourceFiles) {
2001
+ fileContents.set(file.getFilePath(), file.getFullText());
2002
+ }
2003
+ const unused = [];
2004
+ for (const schema of schemas) {
2005
+ const { schemaName, filePath } = schema;
2006
+ let referenceCount = 0;
2007
+ for (const [path, content] of fileContents) {
2008
+ const pattern = new RegExp(`\\b${schemaName}\\b`, "g");
2009
+ const matches = content.match(pattern);
2010
+ const matchCount = matches?.length ?? 0;
2011
+ if (path === filePath) {
2012
+ if (matchCount > 1) {
2013
+ referenceCount += matchCount - 1;
2014
+ }
2015
+ } else {
2016
+ referenceCount += matchCount;
2017
+ }
2018
+ }
2019
+ const fileContent = fileContents.get(filePath) ?? "";
2020
+ const exportPattern = new RegExp(
2021
+ `export\\s+(?:const|let|var)\\s+${schemaName}\\b|export\\s*\\{[^}]*\\b${schemaName}\\b`
2022
+ );
2023
+ if (exportPattern.test(fileContent)) {
2024
+ referenceCount++;
2025
+ }
2026
+ if (referenceCount === 0) {
2027
+ unused.push(schema);
2028
+ }
2029
+ }
2030
+ return unused;
2031
+ }
2032
+ };
2033
+
1916
2034
  // src/dependency-graph.ts
1917
2035
  import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
1918
2036
  import { join as join5, resolve } from "path";
@@ -2453,15 +2571,13 @@ var DetailedAnalyzer = class {
2453
2571
  import { createHash as createHash2 } from "crypto";
2454
2572
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
2455
2573
  import { join as join7, relative } from "path";
2456
- var SNAPSHOT_DIR = ".schemashift";
2457
- var SNAPSHOT_FILE = "schema-snapshot.json";
2458
2574
  var SNAPSHOT_VERSION = 1;
2459
2575
  var DriftDetector = class {
2460
2576
  snapshotDir;
2461
2577
  snapshotPath;
2462
2578
  constructor(projectPath) {
2463
- this.snapshotDir = join7(projectPath, SNAPSHOT_DIR);
2464
- this.snapshotPath = join7(this.snapshotDir, SNAPSHOT_FILE);
2579
+ this.snapshotDir = join7(projectPath, SCHEMASHIFT_DIR);
2580
+ this.snapshotPath = join7(this.snapshotDir, SCHEMA_SNAPSHOT_FILE);
2465
2581
  }
2466
2582
  /**
2467
2583
  * Take a snapshot of the current schema state
@@ -2731,7 +2847,8 @@ var GovernanceEngine = class {
2731
2847
  if (this.rules.has("naming-convention")) {
2732
2848
  const config = this.rules.get("naming-convention") ?? {};
2733
2849
  const pattern = config.pattern || ".*Schema$";
2734
- if (!new RegExp(pattern).test(schemaName)) {
2850
+ const regex = this.safeRegExp(pattern);
2851
+ if (regex && !regex.test(schemaName)) {
2735
2852
  violations.push({
2736
2853
  rule: "naming-convention",
2737
2854
  message: `Schema "${schemaName}" does not match naming pattern: ${pattern}`,
@@ -2893,6 +3010,14 @@ var GovernanceEngine = class {
2893
3010
  passed: violations.filter((v) => v.severity === "error").length === 0
2894
3011
  };
2895
3012
  }
3013
+ safeRegExp(pattern) {
3014
+ if (pattern.length > 500) return null;
3015
+ try {
3016
+ return new RegExp(pattern);
3017
+ } catch {
3018
+ return null;
3019
+ }
3020
+ }
2896
3021
  detectFileLibrary(sourceFile) {
2897
3022
  for (const imp of sourceFile.getImportDeclarations()) {
2898
3023
  const lib = detectSchemaLibrary(imp.getModuleSpecifierValue());
@@ -3628,17 +3753,77 @@ var GraphExporter = class {
3628
3753
  }
3629
3754
  };
3630
3755
 
3756
+ // src/import-deduplicator.ts
3757
+ var ImportDeduplicator = class {
3758
+ detect(sourceFiles) {
3759
+ const allGroups = [];
3760
+ for (const file of sourceFiles) {
3761
+ const groups = this.findDuplicatesInFile(file);
3762
+ allGroups.push(...groups);
3763
+ }
3764
+ const totalDuplicates = allGroups.reduce((sum, g) => sum + g.occurrences.length, 0);
3765
+ return {
3766
+ duplicateGroups: allGroups,
3767
+ totalDuplicates,
3768
+ summary: allGroups.length > 0 ? `Found ${allGroups.length} duplicate import group(s) across ${new Set(allGroups.map((g) => g.occurrences[0]?.filePath)).size} file(s). Merge them for cleaner imports.` : "No duplicate imports found."
3769
+ };
3770
+ }
3771
+ findDuplicatesInFile(sourceFile) {
3772
+ const imports = sourceFile.getImportDeclarations();
3773
+ const filePath = sourceFile.getFilePath();
3774
+ const bySource = /* @__PURE__ */ new Map();
3775
+ for (const imp of imports) {
3776
+ const source = imp.getModuleSpecifierValue();
3777
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
3778
+ const namespaceImport = imp.getNamespaceImport()?.getText();
3779
+ const defaultImport = imp.getDefaultImport()?.getText();
3780
+ const importedNames = [];
3781
+ if (defaultImport) importedNames.push(defaultImport);
3782
+ if (namespaceImport) importedNames.push(`* as ${namespaceImport}`);
3783
+ importedNames.push(...namedImports);
3784
+ if (importedNames.length === 0) continue;
3785
+ const entry = {
3786
+ source,
3787
+ filePath,
3788
+ lineNumber: imp.getStartLineNumber(),
3789
+ importedNames
3790
+ };
3791
+ const existing = bySource.get(source);
3792
+ if (existing) {
3793
+ existing.push(entry);
3794
+ } else {
3795
+ bySource.set(source, [entry]);
3796
+ }
3797
+ }
3798
+ const groups = [];
3799
+ for (const [source, occurrences] of bySource) {
3800
+ if (occurrences.length <= 1) continue;
3801
+ const allNames = /* @__PURE__ */ new Set();
3802
+ for (const occ of occurrences) {
3803
+ for (const name of occ.importedNames) {
3804
+ allNames.add(name);
3805
+ }
3806
+ }
3807
+ const mergedNames = [...allNames].sort().join(", ");
3808
+ groups.push({
3809
+ source,
3810
+ occurrences,
3811
+ suggestion: `Merge into single import: import { ${mergedNames} } from '${source}';`
3812
+ });
3813
+ }
3814
+ return groups;
3815
+ }
3816
+ };
3817
+
3631
3818
  // src/incremental.ts
3632
3819
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
3633
3820
  import { join as join8 } from "path";
3634
- var STATE_DIR = ".schemashift";
3635
- var STATE_FILE = "incremental.json";
3636
3821
  var IncrementalTracker = class {
3637
3822
  stateDir;
3638
3823
  statePath;
3639
3824
  constructor(projectPath) {
3640
- this.stateDir = join8(projectPath, STATE_DIR);
3641
- this.statePath = join8(this.stateDir, STATE_FILE);
3825
+ this.stateDir = join8(projectPath, SCHEMASHIFT_DIR);
3826
+ this.statePath = join8(this.stateDir, INCREMENTAL_STATE_FILE);
3642
3827
  }
3643
3828
  start(files, from, to) {
3644
3829
  const state = {
@@ -3675,7 +3860,9 @@ var IncrementalTracker = class {
3675
3860
  getState() {
3676
3861
  if (!existsSync8(this.statePath)) return null;
3677
3862
  try {
3678
- return JSON.parse(readFileSync8(this.statePath, "utf-8"));
3863
+ const parsed = JSON.parse(readFileSync8(this.statePath, "utf-8"));
3864
+ if (!this.isValidState(parsed)) return null;
3865
+ return parsed;
3679
3866
  } catch {
3680
3867
  return null;
3681
3868
  }
@@ -3722,6 +3909,11 @@ var IncrementalTracker = class {
3722
3909
  unlinkSync(this.statePath);
3723
3910
  }
3724
3911
  }
3912
+ isValidState(data) {
3913
+ if (typeof data !== "object" || data === null) return false;
3914
+ const obj = data;
3915
+ return typeof obj.migrationId === "string" && typeof obj.from === "string" && typeof obj.to === "string" && typeof obj.startedAt === "string" && Array.isArray(obj.completedFiles) && Array.isArray(obj.remainingFiles) && Array.isArray(obj.failedFiles);
3916
+ }
3725
3917
  saveState(state) {
3726
3918
  if (!existsSync8(this.stateDir)) {
3727
3919
  mkdirSync4(this.stateDir, { recursive: true });
@@ -4045,11 +4237,15 @@ var WebhookNotifier = class {
4045
4237
  const signature = await computeSignature(payload, webhook.secret);
4046
4238
  headers["X-SchemaShift-Signature"] = `sha256=${signature}`;
4047
4239
  }
4240
+ const timeoutMs = webhook.timeoutMs ?? 1e4;
4241
+ const controller = new AbortController();
4242
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
4048
4243
  try {
4049
4244
  const response = await fetch(webhook.url, {
4050
4245
  method: "POST",
4051
4246
  headers,
4052
- body: payload
4247
+ body: payload,
4248
+ signal: controller.signal
4053
4249
  });
4054
4250
  return {
4055
4251
  success: response.ok,
@@ -4057,10 +4253,13 @@ var WebhookNotifier = class {
4057
4253
  error: response.ok ? void 0 : `HTTP ${response.status}: ${response.statusText}`
4058
4254
  };
4059
4255
  } catch (err) {
4256
+ const message = err instanceof Error && err.name === "AbortError" ? `Webhook request timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : String(err);
4060
4257
  return {
4061
4258
  success: false,
4062
- error: err instanceof Error ? err.message : String(err)
4259
+ error: message
4063
4260
  };
4261
+ } finally {
4262
+ clearTimeout(timeoutId);
4064
4263
  }
4065
4264
  }
4066
4265
  /**
@@ -4974,11 +5173,16 @@ var TypeDedupDetector = class {
4974
5173
  }
4975
5174
  };
4976
5175
  export {
5176
+ AUDIT_LOG_FILE,
4977
5177
  ApprovalManager,
5178
+ BACKUP_DIR,
4978
5179
  BehavioralWarningAnalyzer,
4979
5180
  BundleEstimator,
5181
+ CONFIG_FILE_NAMES,
4980
5182
  CompatibilityAnalyzer,
4981
5183
  ComplexityEstimator,
5184
+ DEFAULT_CONFIG_FILE,
5185
+ DeadSchemaDetector,
4982
5186
  DetailedAnalyzer,
4983
5187
  DriftDetector,
4984
5188
  EcosystemAnalyzer,
@@ -4987,16 +5191,22 @@ export {
4987
5191
  GovernanceEngine,
4988
5192
  GovernanceFixer,
4989
5193
  GraphExporter,
5194
+ INCREMENTAL_STATE_FILE,
5195
+ ImportDeduplicator,
4990
5196
  IncrementalTracker,
4991
5197
  MigrationAuditLog,
4992
5198
  MigrationChain,
4993
5199
  MonorepoResolver,
5200
+ PENDING_DIR,
4994
5201
  PackageUpdater,
4995
5202
  PerformanceAnalyzer,
4996
5203
  PluginLoader,
5204
+ SCHEMASHIFT_DIR,
5205
+ SCHEMA_SNAPSHOT_FILE,
4997
5206
  SchemaAnalyzer,
4998
5207
  SchemaDependencyResolver,
4999
5208
  StandardSchemaAdvisor,
5209
+ TESTS_DIR,
5000
5210
  TestScaffolder,
5001
5211
  TransformEngine,
5002
5212
  TypeDedupDetector,