@pgpmjs/ast 0.1.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/LICENSE +23 -0
- package/README.md +99 -0
- package/esm/files/extension/index.js +3 -0
- package/esm/files/extension/reader.js +79 -0
- package/esm/files/extension/writer.js +99 -0
- package/esm/files/index.js +6 -0
- package/esm/files/plan/generator.js +82 -0
- package/esm/files/plan/index.js +5 -0
- package/esm/files/plan/parser.js +297 -0
- package/esm/files/plan/validators.js +181 -0
- package/esm/files/plan/writer.js +142 -0
- package/esm/files/sql/header.js +259 -0
- package/esm/files/sql/index.js +2 -0
- package/esm/files/sql/writer.js +107 -0
- package/esm/files/sql-scripts/index.js +2 -0
- package/esm/files/sql-scripts/reader.js +19 -0
- package/esm/files/types/index.js +1 -0
- package/esm/files/types/package.js +1 -0
- package/esm/hash.js +9 -0
- package/esm/index.js +18 -0
- package/esm/module/index.js +12 -0
- package/esm/module/parse.js +27 -0
- package/esm/module/reader.js +64 -0
- package/esm/module/roundtrip.js +65 -0
- package/esm/module/types.js +1 -0
- package/esm/module/writer.js +45 -0
- package/esm/plan-rename.js +28 -0
- package/esm/utils/author.js +11 -0
- package/files/extension/index.d.ts +2 -0
- package/files/extension/index.js +19 -0
- package/files/extension/reader.d.ts +24 -0
- package/files/extension/reader.js +86 -0
- package/files/extension/writer.d.ts +48 -0
- package/files/extension/writer.js +107 -0
- package/files/index.d.ts +5 -0
- package/files/index.js +22 -0
- package/files/plan/generator.d.ts +23 -0
- package/files/plan/generator.js +90 -0
- package/files/plan/index.d.ts +4 -0
- package/files/plan/index.js +21 -0
- package/files/plan/parser.d.ts +28 -0
- package/files/plan/parser.js +304 -0
- package/files/plan/validators.d.ts +52 -0
- package/files/plan/validators.js +187 -0
- package/files/plan/writer.d.ts +27 -0
- package/files/plan/writer.js +152 -0
- package/files/sql/header.d.ts +114 -0
- package/files/sql/header.js +266 -0
- package/files/sql/index.d.ts +2 -0
- package/files/sql/index.js +18 -0
- package/files/sql/writer.d.ts +12 -0
- package/files/sql/writer.js +114 -0
- package/files/sql-scripts/index.d.ts +1 -0
- package/files/sql-scripts/index.js +18 -0
- package/files/sql-scripts/reader.d.ts +8 -0
- package/files/sql-scripts/reader.js +23 -0
- package/files/types/index.d.ts +46 -0
- package/files/types/index.js +17 -0
- package/files/types/package.d.ts +20 -0
- package/files/types/package.js +2 -0
- package/hash.d.ts +6 -0
- package/hash.js +12 -0
- package/index.d.ts +19 -0
- package/index.js +36 -0
- package/module/index.d.ts +12 -0
- package/module/index.js +28 -0
- package/module/parse.d.ts +23 -0
- package/module/parse.js +31 -0
- package/module/reader.d.ts +13 -0
- package/module/reader.js +67 -0
- package/module/roundtrip.d.ts +22 -0
- package/module/roundtrip.js +68 -0
- package/module/types.d.ts +72 -0
- package/module/types.js +2 -0
- package/module/writer.d.ts +30 -0
- package/module/writer.js +50 -0
- package/package.json +48 -0
- package/plan-rename.d.ts +8 -0
- package/plan-rename.js +31 -0
- package/utils/author.d.ts +5 -0
- package/utils/author.js +14 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Re-export all sql-scripts functionality
|
|
18
|
+
__exportStar(require("./reader"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a SQL script file, return empty string if not found
|
|
3
|
+
*/
|
|
4
|
+
export declare function readScript(basePath: string, scriptType: string, changeName: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Check if a script file exists
|
|
7
|
+
*/
|
|
8
|
+
export declare function scriptExists(basePath: string, scriptType: string, changeName: string): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readScript = readScript;
|
|
4
|
+
exports.scriptExists = scriptExists;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
/**
|
|
8
|
+
* Read a SQL script file, return empty string if not found
|
|
9
|
+
*/
|
|
10
|
+
function readScript(basePath, scriptType, changeName) {
|
|
11
|
+
const scriptPath = (0, path_1.join)(basePath, scriptType, `${changeName}.sql`);
|
|
12
|
+
if (!(0, fs_1.existsSync)(scriptPath)) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return (0, fs_1.readFileSync)(scriptPath, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if a script file exists
|
|
19
|
+
*/
|
|
20
|
+
function scriptExists(basePath, scriptType, changeName) {
|
|
21
|
+
const scriptPath = (0, path_1.join)(basePath, scriptType, `${changeName}.sql`);
|
|
22
|
+
return (0, fs_1.existsSync)(scriptPath);
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export * from './package';
|
|
2
|
+
export interface Change {
|
|
3
|
+
name: string;
|
|
4
|
+
dependencies: string[];
|
|
5
|
+
timestamp?: string;
|
|
6
|
+
planner?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
comment?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PlanFile {
|
|
11
|
+
package: string;
|
|
12
|
+
uri?: string;
|
|
13
|
+
changes: Change[];
|
|
14
|
+
}
|
|
15
|
+
export interface Tag {
|
|
16
|
+
name: string;
|
|
17
|
+
change: string;
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
planner?: string;
|
|
20
|
+
email?: string;
|
|
21
|
+
comment?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ExtendedPlanFile extends PlanFile {
|
|
24
|
+
tags: Tag[];
|
|
25
|
+
}
|
|
26
|
+
export interface ParseError {
|
|
27
|
+
line: number;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ParseResult<T> {
|
|
31
|
+
data?: T;
|
|
32
|
+
errors: ParseError[];
|
|
33
|
+
}
|
|
34
|
+
export interface ResolvedReference {
|
|
35
|
+
type: 'change' | 'tag' | 'sha1' | 'relative';
|
|
36
|
+
target: string;
|
|
37
|
+
resolved?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface PgpmRow {
|
|
40
|
+
deploy: string;
|
|
41
|
+
revert?: string;
|
|
42
|
+
verify?: string;
|
|
43
|
+
content: string;
|
|
44
|
+
deps?: string[];
|
|
45
|
+
name?: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./package"), exports);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PackageAnalysisIssue {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
file?: string;
|
|
5
|
+
fixable?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PackageAnalysisResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
issues: PackageAnalysisIssue[];
|
|
12
|
+
}
|
|
13
|
+
export interface RenameOptions {
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
syncPackageJsonName?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface RenameResult {
|
|
18
|
+
changed: string[];
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
package/hash.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a SHA256 hash (hex) of a string. Pure content-addressing primitive
|
|
3
|
+
* shared by the bundle layer and core; kept here so the artifact layer has a
|
|
4
|
+
* dependency-free hash without reaching into core.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hashString(content: string): string;
|
package/hash.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hashString = hashString;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Generate a SHA256 hash (hex) of a string. Pure content-addressing primitive
|
|
7
|
+
* shared by the bundle layer and core; kept here so the artifact layer has a
|
|
8
|
+
* dependency-free hash without reaching into core.
|
|
9
|
+
*/
|
|
10
|
+
function hashString(content) {
|
|
11
|
+
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
12
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pgpmjs/ast — the pgpm module AST layer.
|
|
3
|
+
*
|
|
4
|
+
* Two things live here:
|
|
5
|
+
* - `files/*` — the low-level file parsers/writers (plan, control, SQL header,
|
|
6
|
+
* scripts) and their types. These are the primitives everything else composes.
|
|
7
|
+
* - `module/*` — the unified {@link PgpmModuleAst}: `readModule`/`writeModule`
|
|
8
|
+
* (parse/deparse a whole pgpm module) built by composing the `files/*` parsers,
|
|
9
|
+
* lossless round-trip.
|
|
10
|
+
*
|
|
11
|
+
* This package is a pure leaf (no DB, no deploy engine); `@pgpmjs/core` and the
|
|
12
|
+
* bundle/artifact layer build on top of it.
|
|
13
|
+
*/
|
|
14
|
+
export * from './files';
|
|
15
|
+
export * from './hash';
|
|
16
|
+
export * from './module';
|
|
17
|
+
export * from './plan-rename';
|
|
18
|
+
export { parseAuthor } from './utils/author';
|
|
19
|
+
export type { ParsedAuthor } from './utils/author';
|
package/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.parseAuthor = void 0;
|
|
18
|
+
/**
|
|
19
|
+
* @pgpmjs/ast — the pgpm module AST layer.
|
|
20
|
+
*
|
|
21
|
+
* Two things live here:
|
|
22
|
+
* - `files/*` — the low-level file parsers/writers (plan, control, SQL header,
|
|
23
|
+
* scripts) and their types. These are the primitives everything else composes.
|
|
24
|
+
* - `module/*` — the unified {@link PgpmModuleAst}: `readModule`/`writeModule`
|
|
25
|
+
* (parse/deparse a whole pgpm module) built by composing the `files/*` parsers,
|
|
26
|
+
* lossless round-trip.
|
|
27
|
+
*
|
|
28
|
+
* This package is a pure leaf (no DB, no deploy engine); `@pgpmjs/core` and the
|
|
29
|
+
* bundle/artifact layer build on top of it.
|
|
30
|
+
*/
|
|
31
|
+
__exportStar(require("./files"), exports);
|
|
32
|
+
__exportStar(require("./hash"), exports);
|
|
33
|
+
__exportStar(require("./module"), exports);
|
|
34
|
+
__exportStar(require("./plan-rename"), exports);
|
|
35
|
+
var author_1 = require("./utils/author");
|
|
36
|
+
Object.defineProperty(exports, "parseAuthor", { enumerable: true, get: function () { return author_1.parseAuthor; } });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgpm-ast: the unified in-memory AST for pgpm modules ("AST for the migrations").
|
|
3
|
+
*
|
|
4
|
+
* Composes the existing file-level parsers (plan, control, SQL header) into a
|
|
5
|
+
* single typed {@link PgpmModuleAst} object that operators transform, then writes
|
|
6
|
+
* it back losslessly.
|
|
7
|
+
*/
|
|
8
|
+
export * from './parse';
|
|
9
|
+
export * from './reader';
|
|
10
|
+
export * from './roundtrip';
|
|
11
|
+
export * from './types';
|
|
12
|
+
export * from './writer';
|
package/module/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* pgpm-ast: the unified in-memory AST for pgpm modules ("AST for the migrations").
|
|
19
|
+
*
|
|
20
|
+
* Composes the existing file-level parsers (plan, control, SQL header) into a
|
|
21
|
+
* single typed {@link PgpmModuleAst} object that operators transform, then writes
|
|
22
|
+
* it back losslessly.
|
|
23
|
+
*/
|
|
24
|
+
__exportStar(require("./parse"), exports);
|
|
25
|
+
__exportStar(require("./reader"), exports);
|
|
26
|
+
__exportStar(require("./roundtrip"), exports);
|
|
27
|
+
__exportStar(require("./types"), exports);
|
|
28
|
+
__exportStar(require("./writer"), exports);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PgpmModuleAst } from './types';
|
|
2
|
+
import { WriteModuleOptions } from './writer';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a pgpm module directory into a {@link PgpmModuleAst}.
|
|
5
|
+
*
|
|
6
|
+
* The module-level analog of `pgsql-parser`'s `parse()`: where that turns SQL
|
|
7
|
+
* text into a PostgreSQL AST, this turns an on-disk pgpm module (plan + control
|
|
8
|
+
* + deploy/revert/verify scripts) into a single typed tree. Alias of
|
|
9
|
+
* {@link readModule}; the inverse of {@link deparseModule}.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseModule(dir: string): PgpmModuleAst;
|
|
12
|
+
/**
|
|
13
|
+
* Deparse a {@link PgpmModuleAst} back to an on-disk pgpm module.
|
|
14
|
+
*
|
|
15
|
+
* The module-level analog of `pgsql-deparser`'s `deparse()`. With the default
|
|
16
|
+
* `fromRaw: true` it is lossless (byte-exact round-trip of {@link parseModule});
|
|
17
|
+
* with `fromRaw: false` it re-serializes the structured model, for operators
|
|
18
|
+
* that mutate the AST first. Alias of {@link writeModule}.
|
|
19
|
+
*
|
|
20
|
+
* For in-memory deparse-to-string of individual parts, use `serializePlan` /
|
|
21
|
+
* `serializeScript`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function deparseModule(module: PgpmModuleAst, options?: WriteModuleOptions): void;
|
package/module/parse.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseModule = parseModule;
|
|
4
|
+
exports.deparseModule = deparseModule;
|
|
5
|
+
const reader_1 = require("./reader");
|
|
6
|
+
const writer_1 = require("./writer");
|
|
7
|
+
/**
|
|
8
|
+
* Parse a pgpm module directory into a {@link PgpmModuleAst}.
|
|
9
|
+
*
|
|
10
|
+
* The module-level analog of `pgsql-parser`'s `parse()`: where that turns SQL
|
|
11
|
+
* text into a PostgreSQL AST, this turns an on-disk pgpm module (plan + control
|
|
12
|
+
* + deploy/revert/verify scripts) into a single typed tree. Alias of
|
|
13
|
+
* {@link readModule}; the inverse of {@link deparseModule}.
|
|
14
|
+
*/
|
|
15
|
+
function parseModule(dir) {
|
|
16
|
+
return (0, reader_1.readModule)(dir);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deparse a {@link PgpmModuleAst} back to an on-disk pgpm module.
|
|
20
|
+
*
|
|
21
|
+
* The module-level analog of `pgsql-deparser`'s `deparse()`. With the default
|
|
22
|
+
* `fromRaw: true` it is lossless (byte-exact round-trip of {@link parseModule});
|
|
23
|
+
* with `fromRaw: false` it re-serializes the structured model, for operators
|
|
24
|
+
* that mutate the AST first. Alias of {@link writeModule}.
|
|
25
|
+
*
|
|
26
|
+
* For in-memory deparse-to-string of individual parts, use `serializePlan` /
|
|
27
|
+
* `serializeScript`.
|
|
28
|
+
*/
|
|
29
|
+
function deparseModule(module, options = {}) {
|
|
30
|
+
(0, writer_1.writeModule)(module, options);
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PgpmModuleAst } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Read a pgpm module directory into the unified {@link PgpmModuleAst}.
|
|
4
|
+
*
|
|
5
|
+
* Composes the existing plan parser, control reader, and SQL header parser — it
|
|
6
|
+
* does not re-implement any parsing. The module is addressed by plan order: the
|
|
7
|
+
* `changes` array follows `pgpm.plan`, and each change eagerly parses its
|
|
8
|
+
* deploy/revert/verify scripts (missing ones are `null`). Every leaf retains its
|
|
9
|
+
* byte-exact source so {@link writeModule} can reproduce the module losslessly.
|
|
10
|
+
*
|
|
11
|
+
* @throws when `dir` has no `pgpm.plan`, or the plan fails to parse.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readModule(dir: string): PgpmModuleAst;
|
package/module/reader.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readModule = readModule;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const reader_1 = require("../files/extension/reader");
|
|
7
|
+
const parser_1 = require("../files/plan/parser");
|
|
8
|
+
const header_1 = require("../files/sql/header");
|
|
9
|
+
const reader_2 = require("../files/sql-scripts/reader");
|
|
10
|
+
function readScriptAst(dir, kind, change) {
|
|
11
|
+
if (!(0, reader_2.scriptExists)(dir, kind, change))
|
|
12
|
+
return null;
|
|
13
|
+
const raw = (0, reader_2.readScript)(dir, kind, change);
|
|
14
|
+
const { header, body } = (0, header_1.parsePgpmHeader)(raw);
|
|
15
|
+
return { kind, header, body, raw };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read a pgpm module directory into the unified {@link PgpmModuleAst}.
|
|
19
|
+
*
|
|
20
|
+
* Composes the existing plan parser, control reader, and SQL header parser — it
|
|
21
|
+
* does not re-implement any parsing. The module is addressed by plan order: the
|
|
22
|
+
* `changes` array follows `pgpm.plan`, and each change eagerly parses its
|
|
23
|
+
* deploy/revert/verify scripts (missing ones are `null`). Every leaf retains its
|
|
24
|
+
* byte-exact source so {@link writeModule} can reproduce the module losslessly.
|
|
25
|
+
*
|
|
26
|
+
* @throws when `dir` has no `pgpm.plan`, or the plan fails to parse.
|
|
27
|
+
*/
|
|
28
|
+
function readModule(dir) {
|
|
29
|
+
const planPath = (0, path_1.join)(dir, 'pgpm.plan');
|
|
30
|
+
if (!(0, fs_1.existsSync)(planPath)) {
|
|
31
|
+
throw new Error(`No pgpm.plan found in module directory: ${dir}`);
|
|
32
|
+
}
|
|
33
|
+
const planRaw = (0, fs_1.readFileSync)(planPath, 'utf-8');
|
|
34
|
+
const parsed = (0, parser_1.parsePlanFile)(planPath);
|
|
35
|
+
if (!parsed.data) {
|
|
36
|
+
const detail = parsed.errors
|
|
37
|
+
.map(e => `line ${e.line}: ${e.message}`)
|
|
38
|
+
.join('; ');
|
|
39
|
+
throw new Error(`Failed to parse plan file ${planPath}: ${detail}`);
|
|
40
|
+
}
|
|
41
|
+
const plan = parsed.data;
|
|
42
|
+
const name = plan.package;
|
|
43
|
+
let control = null;
|
|
44
|
+
if (name) {
|
|
45
|
+
const fileName = `${name}.control`;
|
|
46
|
+
const controlPath = (0, path_1.join)(dir, fileName);
|
|
47
|
+
if ((0, fs_1.existsSync)(controlPath)) {
|
|
48
|
+
const raw = (0, fs_1.readFileSync)(controlPath, 'utf-8');
|
|
49
|
+
const info = (0, reader_1.parseControlFile)(controlPath, dir);
|
|
50
|
+
control = {
|
|
51
|
+
fileName,
|
|
52
|
+
name,
|
|
53
|
+
requires: info.requires,
|
|
54
|
+
version: info.version,
|
|
55
|
+
raw
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const changes = plan.changes.map(planChange => ({
|
|
60
|
+
name: planChange.name,
|
|
61
|
+
plan: planChange,
|
|
62
|
+
deploy: readScriptAst(dir, 'deploy', planChange.name),
|
|
63
|
+
revert: readScriptAst(dir, 'revert', planChange.name),
|
|
64
|
+
verify: readScriptAst(dir, 'verify', planChange.name)
|
|
65
|
+
}));
|
|
66
|
+
return { dir, name, plan, planRaw, control, changes };
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single discrepancy found by {@link verifyModuleRoundTrip}.
|
|
3
|
+
*/
|
|
4
|
+
export interface RoundTripIssue {
|
|
5
|
+
kind: 'script-drift' | 'plan-drift' | 'change-count' | 'change-identity';
|
|
6
|
+
change?: string;
|
|
7
|
+
script?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read-only invariant check for the module AST: proves a module survives a
|
|
12
|
+
* read → serialize → re-read cycle without drifting.
|
|
13
|
+
*
|
|
14
|
+
* Guarantees checked:
|
|
15
|
+
* - every script serializes byte-identically to its source (the SQL header
|
|
16
|
+
* parser is lossless), and
|
|
17
|
+
* - the plan serialization is idempotent and the structured change set is
|
|
18
|
+
* stable across a serialize → re-read round trip.
|
|
19
|
+
*
|
|
20
|
+
* Returns the issues found rather than throwing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verifyModuleRoundTrip(dir: string): RoundTripIssue[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyModuleRoundTrip = verifyModuleRoundTrip;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const os_1 = require("os");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const reader_1 = require("./reader");
|
|
8
|
+
const writer_1 = require("./writer");
|
|
9
|
+
/**
|
|
10
|
+
* Read-only invariant check for the module AST: proves a module survives a
|
|
11
|
+
* read → serialize → re-read cycle without drifting.
|
|
12
|
+
*
|
|
13
|
+
* Guarantees checked:
|
|
14
|
+
* - every script serializes byte-identically to its source (the SQL header
|
|
15
|
+
* parser is lossless), and
|
|
16
|
+
* - the plan serialization is idempotent and the structured change set is
|
|
17
|
+
* stable across a serialize → re-read round trip.
|
|
18
|
+
*
|
|
19
|
+
* Returns the issues found rather than throwing.
|
|
20
|
+
*/
|
|
21
|
+
function verifyModuleRoundTrip(dir) {
|
|
22
|
+
const issues = [];
|
|
23
|
+
const module = (0, reader_1.readModule)(dir);
|
|
24
|
+
for (const change of module.changes) {
|
|
25
|
+
for (const script of [change.deploy, change.revert, change.verify]) {
|
|
26
|
+
if (!script)
|
|
27
|
+
continue;
|
|
28
|
+
if ((0, writer_1.serializeScript)(script) !== script.raw) {
|
|
29
|
+
issues.push({
|
|
30
|
+
kind: 'script-drift',
|
|
31
|
+
change: change.name,
|
|
32
|
+
script: script.kind,
|
|
33
|
+
message: 'header serialize is not byte-identical to source'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const planOnce = (0, writer_1.serializePlan)(module);
|
|
39
|
+
const tmp = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'pgpm-ast-rt-'));
|
|
40
|
+
try {
|
|
41
|
+
(0, writer_1.writeModule)(module, { outDir: tmp, fromRaw: false });
|
|
42
|
+
const reread = (0, reader_1.readModule)(tmp);
|
|
43
|
+
if ((0, writer_1.serializePlan)(reread) !== planOnce) {
|
|
44
|
+
issues.push({ kind: 'plan-drift', message: 'plan serialization is not idempotent' });
|
|
45
|
+
}
|
|
46
|
+
if (reread.changes.length !== module.changes.length) {
|
|
47
|
+
issues.push({
|
|
48
|
+
kind: 'change-count',
|
|
49
|
+
message: `change count drifted: ${module.changes.length} -> ${reread.changes.length}`
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
for (let i = 0; i < module.changes.length; i++) {
|
|
54
|
+
if (reread.changes[i].name !== module.changes[i].name) {
|
|
55
|
+
issues.push({
|
|
56
|
+
kind: 'change-identity',
|
|
57
|
+
change: module.changes[i].name,
|
|
58
|
+
message: `change at index ${i} drifted to "${reread.changes[i].name}"`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
(0, fs_1.rmSync)(tmp, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
return issues;
|
|
68
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PgpmHeader } from '../files/sql/header';
|
|
2
|
+
import { Change, ExtendedPlanFile } from '../files/types';
|
|
3
|
+
/**
|
|
4
|
+
* The three script directions a pgpm change is materialized into on disk.
|
|
5
|
+
*/
|
|
6
|
+
export type PgpmScriptKind = 'deploy' | 'revert' | 'verify';
|
|
7
|
+
/**
|
|
8
|
+
* The parsed AST of a single pgpm SQL script (one of deploy/revert/verify for a
|
|
9
|
+
* change). Carries the structured header (verb/project/change/requires) plus the
|
|
10
|
+
* body, and retains the byte-exact `raw` source so a read→write cycle is lossless.
|
|
11
|
+
*/
|
|
12
|
+
export interface PgpmScriptAst {
|
|
13
|
+
kind: PgpmScriptKind;
|
|
14
|
+
/** Structured header block (see {@link PgpmHeader}). */
|
|
15
|
+
header: PgpmHeader;
|
|
16
|
+
/** Everything after the header block, byte-exact. */
|
|
17
|
+
body: string;
|
|
18
|
+
/** The original file content, byte-exact. */
|
|
19
|
+
raw: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The AST of a single pgpm change: its plan entry (identity + dependencies +
|
|
23
|
+
* attribution) together with the deploy/revert/verify scripts that implement it.
|
|
24
|
+
* A script is `null` when the corresponding file does not exist on disk.
|
|
25
|
+
*/
|
|
26
|
+
export interface PgpmChangeAst {
|
|
27
|
+
/** Path identity / plan change name (e.g. `schemas/auth/tables/users`). */
|
|
28
|
+
name: string;
|
|
29
|
+
/** The plan entry for this change (dependencies, timestamp, planner, ...). */
|
|
30
|
+
plan: Change;
|
|
31
|
+
deploy: PgpmScriptAst | null;
|
|
32
|
+
revert: PgpmScriptAst | null;
|
|
33
|
+
verify: PgpmScriptAst | null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The parsed AST of a module's Postgres extension `.control` file.
|
|
37
|
+
*/
|
|
38
|
+
export interface PgpmControlAst {
|
|
39
|
+
/** The control file name (`<name>.control`). */
|
|
40
|
+
fileName: string;
|
|
41
|
+
/** Extension name (control basename, equals the plan project). */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Extensions this module requires (from the control `requires =` line). */
|
|
44
|
+
requires: string[];
|
|
45
|
+
/** `default_version` from the control file. */
|
|
46
|
+
version: string;
|
|
47
|
+
/** The original file content, byte-exact. */
|
|
48
|
+
raw: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The unified in-memory AST for an entire pgpm module ("AST for the migrations").
|
|
52
|
+
*
|
|
53
|
+
* It composes the existing file-level parsers — plan parser, control reader, SQL
|
|
54
|
+
* header parser — into a single typed object addressed by plan-order changes.
|
|
55
|
+
* Operators (rename, rebundle, slice, transpile) become transforms over this tree
|
|
56
|
+
* instead of ad-hoc file/plan re-reads. Every leaf keeps its byte-exact `raw`, so
|
|
57
|
+
* {@link readModule} → {@link writeModule} is a lossless copy.
|
|
58
|
+
*/
|
|
59
|
+
export interface PgpmModuleAst {
|
|
60
|
+
/** Directory the module was read from. */
|
|
61
|
+
dir: string;
|
|
62
|
+
/** Project (extension) name, from the plan `%project=`. */
|
|
63
|
+
name: string;
|
|
64
|
+
/** Parsed plan (package/uri/changes/tags). */
|
|
65
|
+
plan: ExtendedPlanFile;
|
|
66
|
+
/** The original `pgpm.plan` content, byte-exact. */
|
|
67
|
+
planRaw: string;
|
|
68
|
+
/** Parsed `.control`, or `null` when the module has no control file. */
|
|
69
|
+
control: PgpmControlAst | null;
|
|
70
|
+
/** Changes in plan order, each with its deploy/revert/verify scripts. */
|
|
71
|
+
changes: PgpmChangeAst[];
|
|
72
|
+
}
|
package/module/types.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PgpmModuleAst, PgpmScriptAst } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Serialize a script AST back to SQL text from its structured header + body.
|
|
4
|
+
* Byte-identical to the source for scripts read via {@link readModule} (the SQL
|
|
5
|
+
* header parser is lossless), and reflects any structural header edits.
|
|
6
|
+
*/
|
|
7
|
+
export declare function serializeScript(script: PgpmScriptAst): string;
|
|
8
|
+
/**
|
|
9
|
+
* Serialize a module's plan back to `pgpm.plan` text from its structured model.
|
|
10
|
+
*/
|
|
11
|
+
export declare function serializePlan(module: PgpmModuleAst): string;
|
|
12
|
+
export interface WriteModuleOptions {
|
|
13
|
+
/** Target directory. Defaults to the module's own `dir`. */
|
|
14
|
+
outDir?: string;
|
|
15
|
+
/**
|
|
16
|
+
* When true (default), write each file from its byte-exact `raw`, producing a
|
|
17
|
+
* lossless copy of the source module. Set false to write from the structured
|
|
18
|
+
* model instead (`serializePlan` / `serializeScript`), e.g. after transforms.
|
|
19
|
+
*/
|
|
20
|
+
fromRaw?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Materialize a {@link PgpmModuleAst} back to disk: `pgpm.plan`, the `.control`
|
|
24
|
+
* file (when present), and every change's deploy/revert/verify script.
|
|
25
|
+
*
|
|
26
|
+
* With the default `fromRaw: true` this is a lossless copy of what
|
|
27
|
+
* {@link readModule} loaded; with `fromRaw: false` it emits the structured
|
|
28
|
+
* serialization (used by operators that mutate the AST before writing).
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeModule(module: PgpmModuleAst, options?: WriteModuleOptions): void;
|
package/module/writer.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serializeScript = serializeScript;
|
|
4
|
+
exports.serializePlan = serializePlan;
|
|
5
|
+
exports.writeModule = writeModule;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const writer_1 = require("../files/plan/writer");
|
|
9
|
+
const header_1 = require("../files/sql/header");
|
|
10
|
+
/**
|
|
11
|
+
* Serialize a script AST back to SQL text from its structured header + body.
|
|
12
|
+
* Byte-identical to the source for scripts read via {@link readModule} (the SQL
|
|
13
|
+
* header parser is lossless), and reflects any structural header edits.
|
|
14
|
+
*/
|
|
15
|
+
function serializeScript(script) {
|
|
16
|
+
return (0, header_1.writePgpmScript)({ header: script.header, body: script.body });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Serialize a module's plan back to `pgpm.plan` text from its structured model.
|
|
20
|
+
*/
|
|
21
|
+
function serializePlan(module) {
|
|
22
|
+
return (0, writer_1.generatePlanFileContent)(module.plan);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Materialize a {@link PgpmModuleAst} back to disk: `pgpm.plan`, the `.control`
|
|
26
|
+
* file (when present), and every change's deploy/revert/verify script.
|
|
27
|
+
*
|
|
28
|
+
* With the default `fromRaw: true` this is a lossless copy of what
|
|
29
|
+
* {@link readModule} loaded; with `fromRaw: false` it emits the structured
|
|
30
|
+
* serialization (used by operators that mutate the AST before writing).
|
|
31
|
+
*/
|
|
32
|
+
function writeModule(module, options = {}) {
|
|
33
|
+
const outDir = options.outDir ?? module.dir;
|
|
34
|
+
const fromRaw = options.fromRaw ?? true;
|
|
35
|
+
(0, fs_1.mkdirSync)(outDir, { recursive: true });
|
|
36
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(outDir, 'pgpm.plan'), fromRaw ? module.planRaw : serializePlan(module));
|
|
37
|
+
if (module.control) {
|
|
38
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(outDir, module.control.fileName), module.control.raw);
|
|
39
|
+
}
|
|
40
|
+
for (const change of module.changes) {
|
|
41
|
+
const scripts = [change.deploy, change.revert, change.verify];
|
|
42
|
+
for (const script of scripts) {
|
|
43
|
+
if (!script)
|
|
44
|
+
continue;
|
|
45
|
+
const file = (0, path_1.join)(outDir, script.kind, `${change.name}.sql`);
|
|
46
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(file), { recursive: true });
|
|
47
|
+
(0, fs_1.writeFileSync)(file, fromRaw ? script.raw : serializeScript(script));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|