@noego/proper 0.2.1 → 0.3.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/bin/index.d.mts CHANGED
@@ -6,6 +6,11 @@ interface MigrationConfig {
6
6
  migration_table: string;
7
7
  migration_folder: string;
8
8
  database: string;
9
+ /**
10
+ * Exact canonical migration keys whose historical rounded stamps are
11
+ * explicitly allowed. Proper remains strict for every other migration.
12
+ */
13
+ legacy_migration_keys?: readonly string[];
9
14
  /**
10
15
  * Folder containing one-shot ledger patch files. Defaults to a `patches`
11
16
  * sibling of `migration_folder` (e.g. `migrations` -> `patches`,
package/bin/index.d.ts CHANGED
@@ -6,6 +6,11 @@ interface MigrationConfig {
6
6
  migration_table: string;
7
7
  migration_folder: string;
8
8
  database: string;
9
+ /**
10
+ * Exact canonical migration keys whose historical rounded stamps are
11
+ * explicitly allowed. Proper remains strict for every other migration.
12
+ */
13
+ legacy_migration_keys?: readonly string[];
9
14
  /**
10
15
  * Folder containing one-shot ledger patch files. Defaults to a `patches`
11
16
  * sibling of `migration_folder` (e.g. `migrations` -> `patches`,
package/bin/index.js CHANGED
@@ -90,9 +90,6 @@ module.exports = __toCommonJS(index_exports);
90
90
  // framework/MigrationRunner.ts
91
91
  var import_fs6 = __toESM(require("fs"));
92
92
 
93
- // framework/MigrationDirectoryReader.ts
94
- var import_fs2 = __toESM(require("fs"));
95
-
96
93
  // framework/errors.ts
97
94
  var MigrationError = class _MigrationError extends Error {
98
95
  constructor(message) {
@@ -157,6 +154,16 @@ var DatabaseConnectionError = class _DatabaseConnectionError extends MigrationEr
157
154
  return new _DatabaseConnectionError(`Authentication failed for ${dbType} database`);
158
155
  }
159
156
  };
157
+ var MigrationStampError = class _MigrationStampError extends MigrationError {
158
+ constructor(migrationKey) {
159
+ super(
160
+ `Migration '${migrationKey}' cannot be migrated: its timestamp ends in "000", so it does not look like it was generated by 'proper create'. Recreate it with 'proper create <name>' and move the SQL into the generated files.`
161
+ );
162
+ this.migrationKey = migrationKey;
163
+ this.name = "MigrationStampError";
164
+ Object.setPrototypeOf(this, _MigrationStampError.prototype);
165
+ }
166
+ };
160
167
  var MigrationExecutionError = class _MigrationExecutionError extends MigrationError {
161
168
  constructor(message, migrationName, sql, originalError) {
162
169
  let fullMessage = `Migration Execution Error${migrationName ? ` in '${migrationName}'` : ""}: ${message}`;
@@ -285,6 +292,71 @@ var CLIError = class _CLIError extends MigrationError {
285
292
  }
286
293
  };
287
294
 
295
+ // framework/MigrationManifest.ts
296
+ var import_fs = __toESM(require("fs"));
297
+ var import_path = __toESM(require("path"));
298
+ var MIGRATION_FILE_REGEX = /(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i;
299
+ function isMigrationFile(fileName) {
300
+ return MIGRATION_FILE_REGEX.test(fileName);
301
+ }
302
+ function canonicalMigrationKey(value) {
303
+ return value.replace(MIGRATION_FILE_REGEX, "").toLowerCase();
304
+ }
305
+ function mintMigrationStamp(now = Date.now()) {
306
+ return now % 10 === 0 ? now + 1 : now;
307
+ }
308
+ function isHandStampedKey(key) {
309
+ const stamp = key.split("_")[0];
310
+ return /^\d+$/.test(stamp) && /000$/.test(stamp);
311
+ }
312
+ function resolveMigrationFile(directory, baseName, direction, dialect) {
313
+ const dialectExt = dialect === "sql" ? "mysql" : dialect;
314
+ const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
315
+ if (import_fs.default.existsSync(dialectFile)) return dialectFile;
316
+ const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
317
+ if (import_fs.default.existsSync(genericFile)) return genericFile;
318
+ return null;
319
+ }
320
+ function loadMigrationManifest(directory, dialect) {
321
+ const manifest = /* @__PURE__ */ new Map();
322
+ if (!import_fs.default.existsSync(directory)) return manifest;
323
+ const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name).filter(isMigrationFile);
324
+ const uniqueKeys = /* @__PURE__ */ new Set();
325
+ files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
326
+ uniqueKeys.forEach((key) => {
327
+ manifest.set(key, {
328
+ key,
329
+ upFile: resolveMigrationFile(directory, key, "up", dialect),
330
+ downFile: resolveMigrationFile(directory, key, "down", dialect)
331
+ });
332
+ });
333
+ return manifest;
334
+ }
335
+
336
+ // framework/MigrationConfig.ts
337
+ function validateLegacyMigrationKeys(value) {
338
+ if (value === void 0) {
339
+ return;
340
+ }
341
+ if (!Array.isArray(value) || value.some((key) => !isExactCanonicalMigrationKey(key))) {
342
+ throw new ConfigurationError(
343
+ "legacy_migration_keys must be an array of nonempty exact canonical migration keys; filenames, paths, wildcards, and nonstrings are not allowed"
344
+ );
345
+ }
346
+ }
347
+ function isExactCanonicalMigrationKey(value) {
348
+ if (typeof value !== "string" || value.length === 0 || value !== value.trim()) {
349
+ return false;
350
+ }
351
+ if (value.includes("/") || value.includes("\\") || ["*", "?", "[", "]", "{", "}"].some((token) => value.includes(token))) {
352
+ return false;
353
+ }
354
+ return !isMigrationFile(value) && canonicalMigrationKey(value) === value;
355
+ }
356
+
357
+ // framework/MigrationDirectoryReader.ts
358
+ var import_fs2 = __toESM(require("fs"));
359
+
288
360
  // framework/MigrationNode.ts
289
361
  var MigrationNode = class {
290
362
  constructor(name) {
@@ -416,40 +488,6 @@ var SqlMigrationBuilder = class {
416
488
  }
417
489
  };
418
490
 
419
- // framework/MigrationManifest.ts
420
- var import_fs = __toESM(require("fs"));
421
- var import_path = __toESM(require("path"));
422
- var MIGRATION_FILE_REGEX = /(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i;
423
- function isMigrationFile(fileName) {
424
- return MIGRATION_FILE_REGEX.test(fileName);
425
- }
426
- function canonicalMigrationKey(value) {
427
- return value.replace(MIGRATION_FILE_REGEX, "").toLowerCase();
428
- }
429
- function resolveMigrationFile(directory, baseName, direction, dialect) {
430
- const dialectExt = dialect === "sql" ? "mysql" : dialect;
431
- const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
432
- if (import_fs.default.existsSync(dialectFile)) return dialectFile;
433
- const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
434
- if (import_fs.default.existsSync(genericFile)) return genericFile;
435
- return null;
436
- }
437
- function loadMigrationManifest(directory, dialect) {
438
- const manifest = /* @__PURE__ */ new Map();
439
- if (!import_fs.default.existsSync(directory)) return manifest;
440
- const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name).filter(isMigrationFile);
441
- const uniqueKeys = /* @__PURE__ */ new Set();
442
- files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
443
- uniqueKeys.forEach((key) => {
444
- manifest.set(key, {
445
- key,
446
- upFile: resolveMigrationFile(directory, key, "up", dialect),
447
- downFile: resolveMigrationFile(directory, key, "down", dialect)
448
- });
449
- });
450
- return manifest;
451
- }
452
-
453
491
  // framework/MigrationDirectoryReader.ts
454
492
  var MigrationDirectoryReader = class {
455
493
  constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
@@ -1337,7 +1375,7 @@ var PatchCreator = class _PatchCreator {
1337
1375
  if (!import_fs5.default.existsSync(this.patchFolder)) {
1338
1376
  import_fs5.default.mkdirSync(this.patchFolder, { recursive: true });
1339
1377
  }
1340
- let stamp = Date.now();
1378
+ let stamp = mintMigrationStamp();
1341
1379
  for (let attempt = 0; attempt < 1e3; attempt++) {
1342
1380
  const filePath = import_path4.default.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
1343
1381
  try {
@@ -1345,7 +1383,7 @@ var PatchCreator = class _PatchCreator {
1345
1383
  return filePath;
1346
1384
  } catch (error) {
1347
1385
  if (error && error.code === "EEXIST") {
1348
- stamp += 1;
1386
+ stamp = mintMigrationStamp(stamp + 1);
1349
1387
  continue;
1350
1388
  }
1351
1389
  throw error;
@@ -1834,6 +1872,7 @@ var MySQLMigrationRunner = class {
1834
1872
  */
1835
1873
  this.preflightPromise = null;
1836
1874
  this.lastPatchResults = [];
1875
+ validateLegacyMigrationKeys(config.legacy_migration_keys);
1837
1876
  }
1838
1877
  setup() {
1839
1878
  return __async(this, null, function* () {
@@ -1939,7 +1978,17 @@ var MySQLMigrationRunner = class {
1939
1978
  }
1940
1979
  migrate(migrationNodes, forward) {
1941
1980
  return __async(this, null, function* () {
1981
+ var _a;
1942
1982
  yield this.setup();
1983
+ if (forward) {
1984
+ const legacyMigrationKeys = new Set((_a = this.config.legacy_migration_keys) != null ? _a : []);
1985
+ for (const node of migrationNodes) {
1986
+ const key = node.get_key();
1987
+ if (isHandStampedKey(key) && !legacyMigrationKeys.has(key) && !(yield node.status()).completed) {
1988
+ throw new MigrationStampError(key);
1989
+ }
1990
+ }
1991
+ }
1943
1992
  for (let node of migrationNodes) {
1944
1993
  try {
1945
1994
  if (forward) {
@@ -2035,6 +2084,7 @@ var FileMigrationConfigReader = class {
2035
2084
  try {
2036
2085
  const fileContent = import_fs6.default.readFileSync(this.configFile);
2037
2086
  const config = JSON.parse(fileContent.toString());
2087
+ validateLegacyMigrationKeys(config.legacy_migration_keys);
2038
2088
  if (!config.migration_folder) {
2039
2089
  throw ConfigurationError.missingRequiredProperty("migration_folder");
2040
2090
  }
@@ -2077,7 +2127,7 @@ var MigrationCreator = class {
2077
2127
  if (!import_fs6.default.existsSync(this.config.migration_folder)) {
2078
2128
  import_fs6.default.mkdirSync(this.config.migration_folder, { recursive: true });
2079
2129
  }
2080
- const now_timestamp = Date.now();
2130
+ const now_timestamp = mintMigrationStamp();
2081
2131
  const filename_up = `${now_timestamp}_${name}.up.sql`;
2082
2132
  const filename_down = `${now_timestamp}_${name}.down.sql`;
2083
2133
  import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `