@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/cli.js +1120 -119
- package/bin/cli.js.map +1 -1
- package/bin/cli.mjs +1166 -122
- package/bin/cli.mjs.map +1 -1
- package/bin/index.d.mts +284 -4
- package/bin/index.d.ts +284 -4
- package/bin/index.js +1100 -108
- package/bin/index.js.map +1 -1
- package/bin/index.mjs +1087 -108
- package/bin/index.mjs.map +1 -1
- package/lib/runner.js.map +1 -1
- package/lib/runner.mjs.map +1 -1
- package/package.json +6 -3
- package/readme.md +92 -0
package/bin/index.d.mts
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
|
-
|
|
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
|
-
|
|
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 };
|