@drift-agent/api-drift-engine 2.5.4
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/README.md +133 -0
- package/index.d.ts +55 -0
- package/index.js +86 -0
- package/install.js +97 -0
- package/package.json +41 -0
- package/run.js +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @drift-agent/api-drift-engine
|
|
2
|
+
|
|
3
|
+
API schema diff engine — detect breaking changes in **OpenAPI**, **GraphQL**, and **gRPC** schemas.
|
|
4
|
+
|
|
5
|
+
Thin npm wrapper around the [`drift-guard`](https://github.com/DriftAgent/api-drift-engine) Go binary. On install, the correct pre-built binary for your platform is downloaded automatically.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @drift-agent/api-drift-engine
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js ≥ 16. The binary is downloaded for your platform (macOS arm64/amd64, Linux arm64/amd64, Windows amd64) during `npm install`.
|
|
14
|
+
|
|
15
|
+
## CLI
|
|
16
|
+
|
|
17
|
+
After installing, the `drift-guard` binary is available as an npm bin:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx drift-guard --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### OpenAPI
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
drift-guard openapi --base old.yaml --head new.yaml
|
|
27
|
+
drift-guard openapi --base old.yaml --head new.yaml --format json
|
|
28
|
+
drift-guard openapi --base old.yaml --head new.yaml --fail-on-breaking
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### GraphQL
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
drift-guard graphql --base old.graphql --head new.graphql
|
|
35
|
+
drift-guard graphql --base old.graphql --head new.graphql --format markdown
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### gRPC / Protobuf
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
drift-guard grpc --base old.proto --head new.proto
|
|
42
|
+
drift-guard grpc --base old.proto --head new.proto --format json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Impact analysis
|
|
46
|
+
|
|
47
|
+
Scan source code for references to each breaking change:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
# From a saved diff JSON
|
|
51
|
+
drift-guard openapi --base old.yaml --head new.yaml --format json > diff.json
|
|
52
|
+
drift-guard impact --diff diff.json --scan ./src
|
|
53
|
+
|
|
54
|
+
# Pipe mode
|
|
55
|
+
drift-guard openapi --base old.yaml --head new.yaml --format json \
|
|
56
|
+
| drift-guard impact --scan ./src
|
|
57
|
+
|
|
58
|
+
# Output as markdown
|
|
59
|
+
drift-guard impact --diff diff.json --scan ./src --format markdown
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Node.js / TypeScript API
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { compareOpenAPI, compareGraphQL, compareGRPC, impact } from "@drift-agent/api-drift-engine";
|
|
66
|
+
|
|
67
|
+
// Diff two OpenAPI schemas
|
|
68
|
+
const result = compareOpenAPI("old.yaml", "new.yaml");
|
|
69
|
+
console.log(result.summary);
|
|
70
|
+
// { total: 3, breaking: 1, non_breaking: 2, info: 0 }
|
|
71
|
+
|
|
72
|
+
// Diff two GraphQL schemas
|
|
73
|
+
const gqlResult = compareGraphQL("old.graphql", "new.graphql");
|
|
74
|
+
|
|
75
|
+
// Diff two Protobuf schemas
|
|
76
|
+
const grpcResult = compareGRPC("old.proto", "new.proto");
|
|
77
|
+
|
|
78
|
+
// Scan source for references to breaking changes
|
|
79
|
+
const hits = impact(result, "./src");
|
|
80
|
+
// Returns Hit[] — file paths and line numbers that reference each breaking change
|
|
81
|
+
|
|
82
|
+
// Text or markdown report
|
|
83
|
+
const report = impact(result, "./src", { format: "markdown" });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### CommonJS
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const { compareOpenAPI, impact } = require("@drift-agent/api-drift-engine");
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## TypeScript types
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
type Severity = "breaking" | "non-breaking" | "info";
|
|
96
|
+
|
|
97
|
+
interface Change {
|
|
98
|
+
type: string; // e.g. "endpoint_removed", "field_type_changed"
|
|
99
|
+
severity: Severity;
|
|
100
|
+
path: string; // e.g. "/users/{id}"
|
|
101
|
+
method: string; // e.g. "DELETE"
|
|
102
|
+
location: string;
|
|
103
|
+
description: string;
|
|
104
|
+
before?: string;
|
|
105
|
+
after?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface Summary {
|
|
109
|
+
total: number;
|
|
110
|
+
breaking: number;
|
|
111
|
+
non_breaking: number;
|
|
112
|
+
info: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface DiffResult {
|
|
116
|
+
base_file: string;
|
|
117
|
+
head_file: string;
|
|
118
|
+
changes: Change[];
|
|
119
|
+
summary: Summary;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface Hit {
|
|
123
|
+
file: string;
|
|
124
|
+
line_num: number;
|
|
125
|
+
line: string;
|
|
126
|
+
change_type: string; // e.g. "endpoint_removed"
|
|
127
|
+
change_path: string; // e.g. "DELETE /users/{id}"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Severity = "breaking" | "non-breaking" | "info";
|
|
2
|
+
|
|
3
|
+
export interface Change {
|
|
4
|
+
type: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
path: string;
|
|
7
|
+
method: string;
|
|
8
|
+
location: string;
|
|
9
|
+
description: string;
|
|
10
|
+
before?: string;
|
|
11
|
+
after?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Summary {
|
|
15
|
+
total: number;
|
|
16
|
+
breaking: number;
|
|
17
|
+
non_breaking: number;
|
|
18
|
+
info: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DiffResult {
|
|
22
|
+
base_file: string;
|
|
23
|
+
head_file: string;
|
|
24
|
+
changes: Change[];
|
|
25
|
+
summary: Summary;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Hit {
|
|
29
|
+
file: string;
|
|
30
|
+
line_num: number;
|
|
31
|
+
line: string;
|
|
32
|
+
change_type: string;
|
|
33
|
+
change_path: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ImpactOptions {
|
|
37
|
+
format?: "text" | "json" | "markdown";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Diff two OpenAPI 3.x schemas (YAML or JSON). */
|
|
41
|
+
export function compareOpenAPI(base: string, head: string): DiffResult;
|
|
42
|
+
|
|
43
|
+
/** Diff two GraphQL SDL schemas (.graphql or .gql). */
|
|
44
|
+
export function compareGraphQL(base: string, head: string): DiffResult;
|
|
45
|
+
|
|
46
|
+
/** Diff two Protobuf schemas (.proto). */
|
|
47
|
+
export function compareGRPC(base: string, head: string): DiffResult;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scan a source directory for references to breaking changes.
|
|
51
|
+
* Returns Hit[] when format is "json" (default), otherwise a formatted string.
|
|
52
|
+
*/
|
|
53
|
+
export function impact(diffResult: DiffResult, scanDir?: string, options?: ImpactOptions & { format: "json" }): Hit[];
|
|
54
|
+
export function impact(diffResult: DiffResult, scanDir?: string, options?: ImpactOptions & { format: "text" | "markdown" }): string;
|
|
55
|
+
export function impact(diffResult: DiffResult, scanDir?: string, options?: ImpactOptions): Hit[] | string;
|
package/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Programmatic API — wraps the drift-guard binary and returns parsed results.
|
|
3
|
+
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawnSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const BIN = path.join(
|
|
8
|
+
__dirname,
|
|
9
|
+
"bin",
|
|
10
|
+
process.platform === "win32" ? "drift-guard.exe" : "drift-guard"
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run the drift-guard binary and return its stdout.
|
|
15
|
+
* @param {string[]} args
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function run(args) {
|
|
19
|
+
const result = spawnSync(BIN, args, { encoding: "utf8" });
|
|
20
|
+
if (result.error) {
|
|
21
|
+
throw new Error(`drift-guard binary error: ${result.error.message}`);
|
|
22
|
+
}
|
|
23
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
24
|
+
// exit 1 means breaking changes found (--fail-on-breaking), not a crash
|
|
25
|
+
throw new Error(`drift-guard exited with code ${result.status}: ${result.stderr}`);
|
|
26
|
+
}
|
|
27
|
+
return result.stdout;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Diff two OpenAPI 3.x schemas.
|
|
32
|
+
* @param {string} base Path to the base (old) schema file
|
|
33
|
+
* @param {string} head Path to the head (new) schema file
|
|
34
|
+
* @returns {DiffResult}
|
|
35
|
+
*/
|
|
36
|
+
function compareOpenAPI(base, head) {
|
|
37
|
+
return JSON.parse(run(["openapi", "--base", base, "--head", head, "--format", "json"]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Diff two GraphQL SDL schemas.
|
|
42
|
+
* @param {string} base
|
|
43
|
+
* @param {string} head
|
|
44
|
+
* @returns {DiffResult}
|
|
45
|
+
*/
|
|
46
|
+
function compareGraphQL(base, head) {
|
|
47
|
+
return JSON.parse(run(["graphql", "--base", base, "--head", head, "--format", "json"]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Diff two Protobuf schemas.
|
|
52
|
+
* @param {string} base
|
|
53
|
+
* @param {string} head
|
|
54
|
+
* @returns {DiffResult}
|
|
55
|
+
*/
|
|
56
|
+
function compareGRPC(base, head) {
|
|
57
|
+
return JSON.parse(run(["grpc", "--base", base, "--head", head, "--format", "json"]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan a directory for source references to breaking changes in a diff result.
|
|
62
|
+
* @param {DiffResult} diffResult
|
|
63
|
+
* @param {string} scanDir Directory to scan (default ".")
|
|
64
|
+
* @param {{ format?: "text"|"json"|"markdown" }} [options]
|
|
65
|
+
* @returns {Hit[]|string} Array of hits when format="json", otherwise formatted string
|
|
66
|
+
*/
|
|
67
|
+
function impact(diffResult, scanDir = ".", options = {}) {
|
|
68
|
+
const { format = "json" } = options;
|
|
69
|
+
const fs = require("fs");
|
|
70
|
+
const os = require("os");
|
|
71
|
+
|
|
72
|
+
// Write the diff result to a temp file so we can pass --diff
|
|
73
|
+
const tmp = require("path").join(os.tmpdir(), `drift-guard-diff-${Date.now()}.json`);
|
|
74
|
+
try {
|
|
75
|
+
fs.writeFileSync(tmp, JSON.stringify(diffResult));
|
|
76
|
+
const out = run(["impact", "--diff", tmp, "--scan", scanDir, "--format", format]);
|
|
77
|
+
if (format === "json") {
|
|
78
|
+
return JSON.parse(out);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
} finally {
|
|
82
|
+
fs.rmSync(tmp, { force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { compareOpenAPI, compareGraphQL, compareGRPC, impact };
|
package/install.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// postinstall: downloads the drift-guard binary from GitHub Releases.
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { execSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const VERSION = require("./package.json").version;
|
|
12
|
+
const REPO = "DriftAgent/api-drift-engine";
|
|
13
|
+
const BIN_DIR = path.join(__dirname, "bin");
|
|
14
|
+
const BIN_PATH = path.join(BIN_DIR, process.platform === "win32" ? "drift-guard.exe" : "drift-guard");
|
|
15
|
+
|
|
16
|
+
// Map Node.js platform/arch → goreleaser archive naming
|
|
17
|
+
function getPlatformInfo() {
|
|
18
|
+
const p = process.platform;
|
|
19
|
+
const a = process.arch;
|
|
20
|
+
|
|
21
|
+
const osMap = { linux: "linux", darwin: "darwin", win32: "windows" };
|
|
22
|
+
const archMap = { x64: "amd64", arm64: "arm64" };
|
|
23
|
+
|
|
24
|
+
const goos = osMap[p];
|
|
25
|
+
const goarch = archMap[a];
|
|
26
|
+
|
|
27
|
+
if (!goos || !goarch) {
|
|
28
|
+
throw new Error(`Unsupported platform: ${p}/${a}`);
|
|
29
|
+
}
|
|
30
|
+
if (goos === "windows" && goarch === "arm64") {
|
|
31
|
+
throw new Error("Windows arm64 is not supported");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ext = goos === "windows" ? "zip" : "tar.gz";
|
|
35
|
+
const archive = `drift-guard_${VERSION}_${goos}_${goarch}.${ext}`;
|
|
36
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archive}`;
|
|
37
|
+
|
|
38
|
+
return { url, archive, ext };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function download(url, dest) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const file = fs.createWriteStream(dest);
|
|
44
|
+
function get(u) {
|
|
45
|
+
https.get(u, (res) => {
|
|
46
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
47
|
+
return get(res.headers.location);
|
|
48
|
+
}
|
|
49
|
+
if (res.statusCode !== 200) {
|
|
50
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode} for ${u}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.pipe(file);
|
|
54
|
+
file.on("finish", () => file.close(resolve));
|
|
55
|
+
}).on("error", reject);
|
|
56
|
+
}
|
|
57
|
+
get(url);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function install() {
|
|
62
|
+
if (fs.existsSync(BIN_PATH)) {
|
|
63
|
+
return; // already installed (e.g. cached node_modules)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { url, archive, ext } = getPlatformInfo();
|
|
67
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "drift-guard-"));
|
|
68
|
+
const archivePath = path.join(tmpDir, archive);
|
|
69
|
+
|
|
70
|
+
process.stdout.write(`Downloading drift-guard v${VERSION}...\n`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await download(url, archivePath);
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
76
|
+
|
|
77
|
+
if (ext === "tar.gz") {
|
|
78
|
+
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`);
|
|
79
|
+
} else {
|
|
80
|
+
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const extracted = path.join(tmpDir, process.platform === "win32" ? "drift-guard.exe" : "drift-guard");
|
|
84
|
+
fs.copyFileSync(extracted, BIN_PATH);
|
|
85
|
+
fs.chmodSync(BIN_PATH, 0o755);
|
|
86
|
+
|
|
87
|
+
process.stdout.write(`drift-guard installed to ${BIN_PATH}\n`);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
install().catch((err) => {
|
|
94
|
+
process.stderr.write(`drift-guard install failed: ${err.message}\n`);
|
|
95
|
+
process.stderr.write("You can install it manually: https://github.com/DriftAgent/api-drift-engine/releases\n");
|
|
96
|
+
// Do not exit(1) — allow npm install to succeed even if binary download fails.
|
|
97
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drift-agent/api-drift-engine",
|
|
3
|
+
"version": "2.5.4",
|
|
4
|
+
"description": "API schema diff engine — detect breaking changes in OpenAPI, GraphQL, and gRPC schemas",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"drift-guard": "run.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node install.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"index.d.ts",
|
|
16
|
+
"run.js",
|
|
17
|
+
"install.js",
|
|
18
|
+
"bin/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"api",
|
|
23
|
+
"openapi",
|
|
24
|
+
"graphql",
|
|
25
|
+
"grpc",
|
|
26
|
+
"schema",
|
|
27
|
+
"diff",
|
|
28
|
+
"breaking-changes",
|
|
29
|
+
"drift"
|
|
30
|
+
],
|
|
31
|
+
"author": "DriftAgent",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/DriftAgent/api-drift-engine"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/DriftAgent/api-drift-engine",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin shim: passes all arguments to the drift-guard binary.
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawnSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const bin = path.join(
|
|
9
|
+
__dirname,
|
|
10
|
+
"bin",
|
|
11
|
+
process.platform === "win32" ? "drift-guard.exe" : "drift-guard"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
15
|
+
|
|
16
|
+
if (result.error) {
|
|
17
|
+
process.stderr.write(
|
|
18
|
+
`drift-guard: could not run binary: ${result.error.message}\n` +
|
|
19
|
+
`Make sure the package installed correctly or download manually:\n` +
|
|
20
|
+
`https://github.com/DriftAgent/api-drift-engine/releases\n`
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.exit(result.status ?? 0);
|