@noego/proper 0.0.9 → 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.ts CHANGED
@@ -1,10 +1,19 @@
1
1
  import mysql from 'mysql2/promise';
2
+ import * as sqlite from 'sqlite';
2
3
 
3
4
  interface MigrationConfig {
4
5
  config_file?: string;
5
6
  migration_table: string;
6
7
  migration_folder: string;
7
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;
8
17
  sql?: {
9
18
  host: string;
10
19
  user: string;
@@ -15,6 +24,20 @@ interface MigrationConfig {
15
24
  sqlite?: {
16
25
  database: string;
17
26
  };
27
+ /**
28
+ * PostgreSQL. Either a single `connectionString` (also readable from
29
+ * `process.env.DATABASE_URL` when omitted) or discrete fields.
30
+ * `password` falls back to `process.env.PG_PASSWORD`.
31
+ */
32
+ pg?: {
33
+ connectionString?: string;
34
+ host?: string;
35
+ user?: string;
36
+ database?: string;
37
+ password?: string;
38
+ port?: number;
39
+ ssl?: boolean | Record<string, unknown>;
40
+ };
18
41
  seeds?: {
19
42
  migrationsDir?: string;
20
43
  dataDir?: string;
@@ -31,6 +54,92 @@ interface ISQLRunner {
31
54
  execute(sql: string, params?: any[]): Promise<any>;
32
55
  end(): Promise<void>;
33
56
  }
57
+ /**
58
+ * Base class that implements the common contract and helper utilities that are
59
+ * shared between the different dialect runners. The concrete subclasses only
60
+ * need to implement the three primitive methods `_query`, `_execute` and
61
+ * `_end` that perform the actual driver-specific interaction. Everything else
62
+ * – such as ensuring a uniform return shape – is handled here once.
63
+ */
64
+ declare abstract class BaseSQLRunner implements ISQLRunner {
65
+ abstract _query(sql: string, params?: any[]): Promise<any>;
66
+ abstract _execute(sql: string, params?: any[]): Promise<any>;
67
+ abstract _end(): Promise<void>;
68
+ /**
69
+ * Ensures that both MySQL and SQLite return the same tuple shape that callers
70
+ * expect: `[rowsOrResult, extra]`. For SQLite there is no `extra` metadata
71
+ * comparable to MySQL's `FieldPacket[]`, so we just use `undefined`.
72
+ */
73
+ query(sql: string, params?: any[]): Promise<any>;
74
+ execute(sql: string, params?: any[]): Promise<any>;
75
+ end(): Promise<void>;
76
+ }
77
+ declare class SQLRunner extends BaseSQLRunner {
78
+ private connection;
79
+ constructor(connection: mysql.Connection);
80
+ _query(sql: string, params?: any[]): Promise<[mysql.OkPacket | mysql.RowDataPacket[] | mysql.ResultSetHeader[] | mysql.RowDataPacket[][] | mysql.OkPacket[] | mysql.ProcedureCallPacket, mysql.FieldPacket[]]>;
81
+ _execute(sql: string, params?: any[]): Promise<[mysql.OkPacket | mysql.RowDataPacket[] | mysql.ResultSetHeader[] | mysql.RowDataPacket[][] | mysql.OkPacket[] | mysql.ProcedureCallPacket, mysql.FieldPacket[]]>;
82
+ _end(): Promise<void>;
83
+ }
84
+ declare class SQLiteRunner extends BaseSQLRunner {
85
+ private connection;
86
+ constructor(connection: sqlite.Database | any);
87
+ private prepareStatement;
88
+ private finalizeStatement;
89
+ private statementAll;
90
+ private statementRun;
91
+ _query(sql: string, params?: any[]): Promise<any>;
92
+ _execute(sql: string, params?: any[]): Promise<any>;
93
+ /**
94
+ * Checks if SQL is empty or contains only comments/whitespace.
95
+ * Returns true if there is no actual SQL to execute.
96
+ */
97
+ private isEmptySQL;
98
+ private isMultiStatement;
99
+ private executeMultiStatement;
100
+ private removeComments;
101
+ _end(): Promise<void>;
102
+ }
103
+ /**
104
+ * Minimal structural type for a `pg` Client or Pool (or anything shaped like
105
+ * one, e.g. a Hyperdrive/Neon client). We only rely on `query()` and `end()`.
106
+ */
107
+ interface PgQueryable {
108
+ query(text: string, values?: any[]): Promise<{
109
+ rows: any[];
110
+ rowCount: number | null;
111
+ }>;
112
+ end?(): Promise<void>;
113
+ }
114
+ /**
115
+ * PostgreSQL runner.
116
+ *
117
+ * Proper's internal bookkeeping statements use MySQL-style `?` placeholders;
118
+ * Postgres wants `$1..$n`, so parameterised statements are rewritten here.
119
+ * Migration files themselves are executed verbatim with no parameters — a
120
+ * parameter-less `query()` goes through the simple protocol, which allows
121
+ * multiple `;`-separated statements in one call, so no client-side splitting
122
+ * (as SQLite needs) is required.
123
+ */
124
+ declare class PgRunner extends BaseSQLRunner {
125
+ private connection;
126
+ constructor(connection: PgQueryable);
127
+ /** `?` -> `$1`, `$2`, ... outside of string literals / comments. */
128
+ static toPositional(sql: string): string;
129
+ private run;
130
+ _query(sql: string, params?: any[]): Promise<(any[] | {
131
+ rows: any[];
132
+ rowCount: number | null;
133
+ })[]>;
134
+ _execute(sql: string, params?: any[]): Promise<({
135
+ rows: any[];
136
+ rowCount: number | null;
137
+ } | {
138
+ changes: number;
139
+ lastID: undefined;
140
+ })[]>;
141
+ _end(): Promise<void>;
142
+ }
34
143
 
35
144
  declare abstract class MigrationNode {
36
145
  name: string;
@@ -76,10 +185,11 @@ declare class MigrationDirectoryReader {
76
185
  private read_strategy;
77
186
  private sqlrunner;
78
187
  private dialect;
79
- constructor(directory: string, read_strategy: any, sqlrunner: ISQLRunner, dialect?: 'sql' | 'sqlite');
188
+ constructor(directory: string, read_strategy: any, sqlrunner: ISQLRunner, dialect?: 'sql' | 'sqlite' | 'pg');
80
189
  /**
81
190
  * Resolves the appropriate file for a migration based on dialect.
82
191
  * Priority: dialect-specific file > generic file
192
+ * File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
83
193
  */
84
194
  private resolveFile;
85
195
  /**
@@ -100,13 +210,57 @@ declare class MigrationSetup {
100
210
  teardown(): Promise<void>;
101
211
  }
102
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
+
103
257
  declare function loadMigrationConfig(configFile: string): MigrationConfig;
104
258
  declare class MigrationRunnerFactory {
105
259
  private static isSQLRunner;
106
260
  static create(configFile: string, conn?: any): Promise<MySQLMigrationRunner>;
107
261
  static createConnection(config: MigrationConfig): Promise<any>;
108
262
  static createEmpty(configFile: string): Promise<MySQLMigrationRunner>;
109
- create(config: MigrationConfig, conn: any): Promise<MySQLMigrationRunner>;
263
+ create(config: MigrationConfig, conn: any, factoryOwnsConnection?: boolean): Promise<MySQLMigrationRunner>;
110
264
  createEmpty(config: MigrationConfig): Promise<MySQLMigrationRunner>;
111
265
  private getReadStategy;
112
266
  }
@@ -117,6 +271,8 @@ interface MigrationHistory {
117
271
  }
118
272
  interface IMigrationRunner {
119
273
  setup(): Promise<void>;
274
+ applyPendingPatches(): Promise<PatchApplyResult[]>;
275
+ createPatch(name: string): string;
120
276
  terminate(): Promise<void>;
121
277
  getMigrationsHistory(): Promise<MigrationHistory[]>;
122
278
  getMigrations(): Promise<MigrationNode[]>;
@@ -135,8 +291,28 @@ declare class MySQLMigrationRunner implements IMigrationRunner {
135
291
  private setupRunner;
136
292
  private sqlrunner;
137
293
  private connection;
138
- 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);
139
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;
140
316
  terminate(): Promise<void>;
141
317
  getMigrationsHistory(): Promise<MigrationHistory[]>;
142
318
  getMigrations(): Promise<MigrationNode[]>;
@@ -176,4 +352,108 @@ type SeedFactory = {
176
352
  };
177
353
  declare function createSeedFactory(options: SeedFactoryOptions): SeedFactory;
178
354
 
179
- export { type MigrationConfig, MySQLMigrationRunner as MigrationRunner, MigrationRunnerFactory, 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 };