@noego/proper 0.1.0 → 0.2.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,14 @@ interface MigrationConfig {
6
6
  migration_table: string;
7
7
  migration_folder: string;
8
8
  database: string;
9
+ /**
10
+ * Folder containing one-shot ledger patch files. Defaults to a `patches`
11
+ * sibling of `migration_folder` (e.g. `migrations` -> `patches`,
12
+ * `database/migrations` -> `database/patches`).
13
+ */
14
+ patch_folder?: string;
15
+ /** Table recording applied patches. Defaults to `proper_patches`. */
16
+ patch_table?: string;
9
17
  sql?: {
10
18
  host: string;
11
19
  user: string;
@@ -202,13 +210,57 @@ declare class MigrationSetup {
202
210
  teardown(): Promise<void>;
203
211
  }
204
212
 
213
+ type PatchOperation = {
214
+ verb: 'rename_migration';
215
+ from: string;
216
+ to: string;
217
+ } | {
218
+ verb: 'mark_applied';
219
+ key: string;
220
+ } | {
221
+ verb: 'unmark_applied';
222
+ key: string;
223
+ };
224
+ /** A parsed and schema-validated patch file. */
225
+ interface PatchDocument {
226
+ /** Filename without `.yaml`. */
227
+ patchKey: string;
228
+ /** Basename including `.yaml`. */
229
+ fileName: string;
230
+ /** Absolute or config-relative resolved path. */
231
+ filePath: string;
232
+ /** SHA-256 hex over the exact UTF-8 file bytes. */
233
+ checksum: string;
234
+ version: number;
235
+ description: string;
236
+ operations: PatchOperation[];
237
+ }
238
+ interface PatchOperationResult {
239
+ verb: PatchOperation['verb'];
240
+ /** True when the operation mutated the ledger; false for a conditional no-op. */
241
+ changed: boolean;
242
+ }
243
+ interface PatchApplyResult {
244
+ patchKey: string;
245
+ fileName: string;
246
+ /**
247
+ * applied - this process committed the patch
248
+ * already_applied - a matching patch-history row already existed
249
+ */
250
+ status: 'applied' | 'already_applied';
251
+ operations: PatchOperationResult[];
252
+ }
253
+ /** Resolved patch settings with defaults applied. */
254
+ declare function resolvePatchFolder(config: MigrationConfig): string;
255
+ declare function resolvePatchTable(config: MigrationConfig): string;
256
+
205
257
  declare function loadMigrationConfig(configFile: string): MigrationConfig;
206
258
  declare class MigrationRunnerFactory {
207
259
  private static isSQLRunner;
208
260
  static create(configFile: string, conn?: any): Promise<MySQLMigrationRunner>;
209
261
  static createConnection(config: MigrationConfig): Promise<any>;
210
262
  static createEmpty(configFile: string): Promise<MySQLMigrationRunner>;
211
- create(config: MigrationConfig, conn: any): Promise<MySQLMigrationRunner>;
263
+ create(config: MigrationConfig, conn: any, factoryOwnsConnection?: boolean): Promise<MySQLMigrationRunner>;
212
264
  createEmpty(config: MigrationConfig): Promise<MySQLMigrationRunner>;
213
265
  private getReadStategy;
214
266
  }
@@ -219,6 +271,8 @@ interface MigrationHistory {
219
271
  }
220
272
  interface IMigrationRunner {
221
273
  setup(): Promise<void>;
274
+ applyPendingPatches(): Promise<PatchApplyResult[]>;
275
+ createPatch(name: string): string;
222
276
  terminate(): Promise<void>;
223
277
  getMigrationsHistory(): Promise<MigrationHistory[]>;
224
278
  getMigrations(): Promise<MigrationNode[]>;
@@ -237,8 +291,28 @@ declare class MySQLMigrationRunner implements IMigrationRunner {
237
291
  private setupRunner;
238
292
  private sqlrunner;
239
293
  private connection;
240
- constructor(config: MigrationConfig, directory: MigrationDirectoryReader, setupRunner: MigrationSetup, sqlrunner: ISQLRunner, connection: mysql.Connection);
294
+ private preflightEnabled;
295
+ /**
296
+ * Memoized in-flight preflight promise. Simultaneous or repeated calls
297
+ * to setup() on one runner execute the preflight (migration table setup
298
+ * + patch application) exactly once. Cleared after rejection so a caller
299
+ * may retry after fixing the cause.
300
+ */
301
+ private preflightPromise;
302
+ private lastPatchResults;
303
+ constructor(config: MigrationConfig, directory: MigrationDirectoryReader, setupRunner: MigrationSetup, sqlrunner: ISQLRunner, connection: mysql.Connection, preflightEnabled?: boolean);
241
304
  setup(): Promise<void>;
305
+ private runPreflight;
306
+ /**
307
+ * Delegates to the same idempotent preflight; returns the results of the
308
+ * patch pass that ran (or is running) for this runner.
309
+ */
310
+ applyPendingPatches(): Promise<PatchApplyResult[]>;
311
+ /**
312
+ * Scaffolds a new ledger patch file and returns the created path.
313
+ * Never connects to a database.
314
+ */
315
+ createPatch(name: string): string;
242
316
  terminate(): Promise<void>;
243
317
  getMigrationsHistory(): Promise<MigrationHistory[]>;
244
318
  getMigrations(): Promise<MigrationNode[]>;
@@ -278,4 +352,108 @@ type SeedFactory = {
278
352
  };
279
353
  declare function createSeedFactory(options: SeedFactoryOptions): SeedFactory;
280
354
 
281
- export { type ISQLRunner, type MigrationConfig, MySQLMigrationRunner as MigrationRunner, MigrationRunnerFactory, type PgQueryable, PgRunner, SQLRunner, SQLiteRunner, type SeedContext, type SeedFactory, type SeedFactoryOptions, createSeedFactory, loadMigrationConfig, runSeedsWithRunner };
355
+ /**
356
+ * Scaffolds a new patch file. Never connects to a database.
357
+ */
358
+ declare class PatchCreator {
359
+ private patchFolder;
360
+ constructor(patchFolder: string);
361
+ /**
362
+ * Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
363
+ * Rejects empty results, path separators, `..`, control characters,
364
+ * characters outside [a-z0-9_-], and names longer than 120 characters.
365
+ */
366
+ static normalizeName(name: string): string;
367
+ /**
368
+ * Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
369
+ * file creation. On a millisecond-stamp collision, mints a later stamp
370
+ * and retries. Returns the created path.
371
+ */
372
+ create(name: string): string;
373
+ }
374
+
375
+ /**
376
+ * Applies pending ledger patches. Uses ISQLRunner directly (never the public
377
+ * runner methods) so it can run inside the runner's preflight without
378
+ * recursion.
379
+ *
380
+ * Precondition: the provided connection must not already be inside an
381
+ * application-managed transaction when preflight begins; Proper will issue
382
+ * its own BEGIN/COMMIT/ROLLBACK per patch and must not commit or roll back a
383
+ * caller's outer transaction.
384
+ */
385
+ declare class PatchRunner {
386
+ private sqlrunner;
387
+ private config;
388
+ private patchTable;
389
+ private migrationTable;
390
+ private dialect;
391
+ constructor(sqlrunner: ISQLRunner, config: MigrationConfig);
392
+ /**
393
+ * Discovers, validates, and applies every unapplied patch in order.
394
+ * Each unapplied patch is its own transaction; earlier committed patches
395
+ * remain committed if a later patch fails.
396
+ */
397
+ applyPending(): Promise<PatchApplyResult[]>;
398
+ private loadHistory;
399
+ private beginSql;
400
+ private begin;
401
+ private rollbackQuietly;
402
+ private applyOne;
403
+ private findCommittedRow;
404
+ private countRows;
405
+ private conflict;
406
+ private applyOperation;
407
+ }
408
+
409
+ /**
410
+ * Base class for all migration-related errors
411
+ */
412
+ declare class MigrationError extends Error {
413
+ constructor(message: string);
414
+ }
415
+ /**
416
+ * Base class for all patch-related errors.
417
+ */
418
+ declare class PatchError extends MigrationError {
419
+ readonly patchFile?: string | undefined;
420
+ readonly patchKey?: string | undefined;
421
+ constructor(message: string, patchFile?: string | undefined, patchKey?: string | undefined);
422
+ }
423
+ /**
424
+ * A patch file failed YAML parsing or schema/plan validation.
425
+ */
426
+ declare class PatchValidationError extends PatchError {
427
+ constructor(message: string, patchFile?: string, patchKey?: string);
428
+ }
429
+ /**
430
+ * An applied patch's file is missing or its content no longer matches the
431
+ * checksum recorded at application time.
432
+ */
433
+ declare class PatchIntegrityError extends PatchError {
434
+ readonly expectedChecksum?: string | undefined;
435
+ readonly actualChecksum?: string | undefined;
436
+ constructor(message: string, patchFile?: string, patchKey?: string, expectedChecksum?: string | undefined, actualChecksum?: string | undefined);
437
+ }
438
+ /**
439
+ * An operation precondition failed: ledger conflict or corruption
440
+ * (unexpected row counts) at the operation's turn.
441
+ */
442
+ declare class PatchConflictError extends PatchError {
443
+ readonly operationIndex?: number | undefined;
444
+ readonly operationVerb?: string | undefined;
445
+ readonly migrationKeys?: string[] | undefined;
446
+ readonly observedRowCounts?: Record<string, number> | undefined;
447
+ constructor(message: string, patchFile?: string, patchKey?: string, operationIndex?: number | undefined, operationVerb?: string | undefined, migrationKeys?: string[] | undefined, observedRowCounts?: Record<string, number> | undefined);
448
+ }
449
+ /**
450
+ * A database/transaction failure while applying a patch.
451
+ */
452
+ declare class PatchExecutionError extends PatchError {
453
+ readonly operationIndex?: number | undefined;
454
+ readonly operationVerb?: string | undefined;
455
+ readonly originalError?: Error | undefined;
456
+ constructor(message: string, patchFile?: string, patchKey?: string, operationIndex?: number | undefined, operationVerb?: string | undefined, originalError?: Error | undefined);
457
+ }
458
+
459
+ export { type ISQLRunner, type MigrationConfig, MigrationError, MySQLMigrationRunner as MigrationRunner, MigrationRunnerFactory, type PatchApplyResult, PatchConflictError, PatchCreator, type PatchDocument, PatchError, PatchExecutionError, PatchIntegrityError, type PatchOperation, type PatchOperationResult, PatchRunner, PatchValidationError, type PgQueryable, PgRunner, SQLRunner, SQLiteRunner, type SeedContext, type SeedFactory, type SeedFactoryOptions, createSeedFactory, loadMigrationConfig, resolvePatchFolder, resolvePatchTable, runSeedsWithRunner };
package/bin/index.d.ts CHANGED
@@ -6,6 +6,14 @@ interface MigrationConfig {
6
6
  migration_table: string;
7
7
  migration_folder: string;
8
8
  database: string;
9
+ /**
10
+ * Folder containing one-shot ledger patch files. Defaults to a `patches`
11
+ * sibling of `migration_folder` (e.g. `migrations` -> `patches`,
12
+ * `database/migrations` -> `database/patches`).
13
+ */
14
+ patch_folder?: string;
15
+ /** Table recording applied patches. Defaults to `proper_patches`. */
16
+ patch_table?: string;
9
17
  sql?: {
10
18
  host: string;
11
19
  user: string;
@@ -202,13 +210,57 @@ declare class MigrationSetup {
202
210
  teardown(): Promise<void>;
203
211
  }
204
212
 
213
+ type PatchOperation = {
214
+ verb: 'rename_migration';
215
+ from: string;
216
+ to: string;
217
+ } | {
218
+ verb: 'mark_applied';
219
+ key: string;
220
+ } | {
221
+ verb: 'unmark_applied';
222
+ key: string;
223
+ };
224
+ /** A parsed and schema-validated patch file. */
225
+ interface PatchDocument {
226
+ /** Filename without `.yaml`. */
227
+ patchKey: string;
228
+ /** Basename including `.yaml`. */
229
+ fileName: string;
230
+ /** Absolute or config-relative resolved path. */
231
+ filePath: string;
232
+ /** SHA-256 hex over the exact UTF-8 file bytes. */
233
+ checksum: string;
234
+ version: number;
235
+ description: string;
236
+ operations: PatchOperation[];
237
+ }
238
+ interface PatchOperationResult {
239
+ verb: PatchOperation['verb'];
240
+ /** True when the operation mutated the ledger; false for a conditional no-op. */
241
+ changed: boolean;
242
+ }
243
+ interface PatchApplyResult {
244
+ patchKey: string;
245
+ fileName: string;
246
+ /**
247
+ * applied - this process committed the patch
248
+ * already_applied - a matching patch-history row already existed
249
+ */
250
+ status: 'applied' | 'already_applied';
251
+ operations: PatchOperationResult[];
252
+ }
253
+ /** Resolved patch settings with defaults applied. */
254
+ declare function resolvePatchFolder(config: MigrationConfig): string;
255
+ declare function resolvePatchTable(config: MigrationConfig): string;
256
+
205
257
  declare function loadMigrationConfig(configFile: string): MigrationConfig;
206
258
  declare class MigrationRunnerFactory {
207
259
  private static isSQLRunner;
208
260
  static create(configFile: string, conn?: any): Promise<MySQLMigrationRunner>;
209
261
  static createConnection(config: MigrationConfig): Promise<any>;
210
262
  static createEmpty(configFile: string): Promise<MySQLMigrationRunner>;
211
- create(config: MigrationConfig, conn: any): Promise<MySQLMigrationRunner>;
263
+ create(config: MigrationConfig, conn: any, factoryOwnsConnection?: boolean): Promise<MySQLMigrationRunner>;
212
264
  createEmpty(config: MigrationConfig): Promise<MySQLMigrationRunner>;
213
265
  private getReadStategy;
214
266
  }
@@ -219,6 +271,8 @@ interface MigrationHistory {
219
271
  }
220
272
  interface IMigrationRunner {
221
273
  setup(): Promise<void>;
274
+ applyPendingPatches(): Promise<PatchApplyResult[]>;
275
+ createPatch(name: string): string;
222
276
  terminate(): Promise<void>;
223
277
  getMigrationsHistory(): Promise<MigrationHistory[]>;
224
278
  getMigrations(): Promise<MigrationNode[]>;
@@ -237,8 +291,28 @@ declare class MySQLMigrationRunner implements IMigrationRunner {
237
291
  private setupRunner;
238
292
  private sqlrunner;
239
293
  private connection;
240
- constructor(config: MigrationConfig, directory: MigrationDirectoryReader, setupRunner: MigrationSetup, sqlrunner: ISQLRunner, connection: mysql.Connection);
294
+ private preflightEnabled;
295
+ /**
296
+ * Memoized in-flight preflight promise. Simultaneous or repeated calls
297
+ * to setup() on one runner execute the preflight (migration table setup
298
+ * + patch application) exactly once. Cleared after rejection so a caller
299
+ * may retry after fixing the cause.
300
+ */
301
+ private preflightPromise;
302
+ private lastPatchResults;
303
+ constructor(config: MigrationConfig, directory: MigrationDirectoryReader, setupRunner: MigrationSetup, sqlrunner: ISQLRunner, connection: mysql.Connection, preflightEnabled?: boolean);
241
304
  setup(): Promise<void>;
305
+ private runPreflight;
306
+ /**
307
+ * Delegates to the same idempotent preflight; returns the results of the
308
+ * patch pass that ran (or is running) for this runner.
309
+ */
310
+ applyPendingPatches(): Promise<PatchApplyResult[]>;
311
+ /**
312
+ * Scaffolds a new ledger patch file and returns the created path.
313
+ * Never connects to a database.
314
+ */
315
+ createPatch(name: string): string;
242
316
  terminate(): Promise<void>;
243
317
  getMigrationsHistory(): Promise<MigrationHistory[]>;
244
318
  getMigrations(): Promise<MigrationNode[]>;
@@ -278,4 +352,108 @@ type SeedFactory = {
278
352
  };
279
353
  declare function createSeedFactory(options: SeedFactoryOptions): SeedFactory;
280
354
 
281
- export { type ISQLRunner, type MigrationConfig, MySQLMigrationRunner as MigrationRunner, MigrationRunnerFactory, type PgQueryable, PgRunner, SQLRunner, SQLiteRunner, type SeedContext, type SeedFactory, type SeedFactoryOptions, createSeedFactory, loadMigrationConfig, runSeedsWithRunner };
355
+ /**
356
+ * Scaffolds a new patch file. Never connects to a database.
357
+ */
358
+ declare class PatchCreator {
359
+ private patchFolder;
360
+ constructor(patchFolder: string);
361
+ /**
362
+ * Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
363
+ * Rejects empty results, path separators, `..`, control characters,
364
+ * characters outside [a-z0-9_-], and names longer than 120 characters.
365
+ */
366
+ static normalizeName(name: string): string;
367
+ /**
368
+ * Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
369
+ * file creation. On a millisecond-stamp collision, mints a later stamp
370
+ * and retries. Returns the created path.
371
+ */
372
+ create(name: string): string;
373
+ }
374
+
375
+ /**
376
+ * Applies pending ledger patches. Uses ISQLRunner directly (never the public
377
+ * runner methods) so it can run inside the runner's preflight without
378
+ * recursion.
379
+ *
380
+ * Precondition: the provided connection must not already be inside an
381
+ * application-managed transaction when preflight begins; Proper will issue
382
+ * its own BEGIN/COMMIT/ROLLBACK per patch and must not commit or roll back a
383
+ * caller's outer transaction.
384
+ */
385
+ declare class PatchRunner {
386
+ private sqlrunner;
387
+ private config;
388
+ private patchTable;
389
+ private migrationTable;
390
+ private dialect;
391
+ constructor(sqlrunner: ISQLRunner, config: MigrationConfig);
392
+ /**
393
+ * Discovers, validates, and applies every unapplied patch in order.
394
+ * Each unapplied patch is its own transaction; earlier committed patches
395
+ * remain committed if a later patch fails.
396
+ */
397
+ applyPending(): Promise<PatchApplyResult[]>;
398
+ private loadHistory;
399
+ private beginSql;
400
+ private begin;
401
+ private rollbackQuietly;
402
+ private applyOne;
403
+ private findCommittedRow;
404
+ private countRows;
405
+ private conflict;
406
+ private applyOperation;
407
+ }
408
+
409
+ /**
410
+ * Base class for all migration-related errors
411
+ */
412
+ declare class MigrationError extends Error {
413
+ constructor(message: string);
414
+ }
415
+ /**
416
+ * Base class for all patch-related errors.
417
+ */
418
+ declare class PatchError extends MigrationError {
419
+ readonly patchFile?: string | undefined;
420
+ readonly patchKey?: string | undefined;
421
+ constructor(message: string, patchFile?: string | undefined, patchKey?: string | undefined);
422
+ }
423
+ /**
424
+ * A patch file failed YAML parsing or schema/plan validation.
425
+ */
426
+ declare class PatchValidationError extends PatchError {
427
+ constructor(message: string, patchFile?: string, patchKey?: string);
428
+ }
429
+ /**
430
+ * An applied patch's file is missing or its content no longer matches the
431
+ * checksum recorded at application time.
432
+ */
433
+ declare class PatchIntegrityError extends PatchError {
434
+ readonly expectedChecksum?: string | undefined;
435
+ readonly actualChecksum?: string | undefined;
436
+ constructor(message: string, patchFile?: string, patchKey?: string, expectedChecksum?: string | undefined, actualChecksum?: string | undefined);
437
+ }
438
+ /**
439
+ * An operation precondition failed: ledger conflict or corruption
440
+ * (unexpected row counts) at the operation's turn.
441
+ */
442
+ declare class PatchConflictError extends PatchError {
443
+ readonly operationIndex?: number | undefined;
444
+ readonly operationVerb?: string | undefined;
445
+ readonly migrationKeys?: string[] | undefined;
446
+ readonly observedRowCounts?: Record<string, number> | undefined;
447
+ constructor(message: string, patchFile?: string, patchKey?: string, operationIndex?: number | undefined, operationVerb?: string | undefined, migrationKeys?: string[] | undefined, observedRowCounts?: Record<string, number> | undefined);
448
+ }
449
+ /**
450
+ * A database/transaction failure while applying a patch.
451
+ */
452
+ declare class PatchExecutionError extends PatchError {
453
+ readonly operationIndex?: number | undefined;
454
+ readonly operationVerb?: string | undefined;
455
+ readonly originalError?: Error | undefined;
456
+ constructor(message: string, patchFile?: string, patchKey?: string, operationIndex?: number | undefined, operationVerb?: string | undefined, originalError?: Error | undefined);
457
+ }
458
+
459
+ export { type ISQLRunner, type MigrationConfig, MigrationError, MySQLMigrationRunner as MigrationRunner, MigrationRunnerFactory, type PatchApplyResult, PatchConflictError, PatchCreator, type PatchDocument, PatchError, PatchExecutionError, PatchIntegrityError, type PatchOperation, type PatchOperationResult, PatchRunner, PatchValidationError, type PgQueryable, PgRunner, SQLRunner, SQLiteRunner, type SeedContext, type SeedFactory, type SeedFactoryOptions, createSeedFactory, loadMigrationConfig, resolvePatchFolder, resolvePatchTable, runSeedsWithRunner };