post-api-sync 0.1.2 → 0.1.7
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 +17 -4
- package/bin/post-api-sync.js +6 -1
- package/package.json +6 -1
- package/src/collection/postman.js +70 -7
- package/src/config.js +2 -2
- package/src/merge/postman.js +52 -20
- package/src/sync.js +9 -3
- package/src/watch.js +2 -2
package/README.md
CHANGED
|
@@ -59,6 +59,11 @@ module.exports = {
|
|
|
59
59
|
baseUrl: 'http://localhost:3000'
|
|
60
60
|
},
|
|
61
61
|
|
|
62
|
+
organization: {
|
|
63
|
+
// 'folder' (default) or 'tags'
|
|
64
|
+
groupBy: 'folder'
|
|
65
|
+
},
|
|
66
|
+
|
|
62
67
|
output: {
|
|
63
68
|
postman: {
|
|
64
69
|
enabled: true,
|
|
@@ -67,14 +72,18 @@ module.exports = {
|
|
|
67
72
|
apiKey: process.env.POSTMAN_API_KEY,
|
|
68
73
|
collectionId: process.env.POSTMAN_COLLECTION_ID
|
|
69
74
|
},
|
|
70
|
-
|
|
71
|
-
enabled: true,
|
|
72
|
-
outputPath: './insomnia_collection.json'
|
|
73
|
-
}
|
|
75
|
+
// ...
|
|
74
76
|
}
|
|
75
77
|
};
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
### Environment Variables
|
|
81
|
+
You can use a `.env` file in your project root to store sensitive keys:
|
|
82
|
+
```bash
|
|
83
|
+
POSTMAN_API_KEY=your-api-key
|
|
84
|
+
POSTMAN_COLLECTION_ID=your-collection-uid
|
|
85
|
+
```
|
|
86
|
+
|
|
78
87
|
## Postman Cloud Sync
|
|
79
88
|
|
|
80
89
|
You can push your generated collection directly to Postman without manual importing.
|
|
@@ -84,6 +93,10 @@ You can push your generated collection directly to Postman without manual import
|
|
|
84
93
|
3. Run the sync command:
|
|
85
94
|
|
|
86
95
|
```bash
|
|
96
|
+
# Finds keys in .env or config
|
|
97
|
+
npx post-api-sync
|
|
98
|
+
|
|
99
|
+
# Or specify manually
|
|
87
100
|
npx post-api-sync sync --postman-key <YOUR_KEY> --postman-id <COLLECTION_UID>
|
|
88
101
|
```
|
|
89
102
|
|
package/bin/post-api-sync.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
require('dotenv').config();
|
|
3
4
|
const { program } = require('commander');
|
|
4
5
|
const { initConfig } = require('../src/init');
|
|
5
6
|
const { syncOnce } = require('../src/sync');
|
|
@@ -7,7 +8,11 @@ const { watchMode } = require('../src/watch');
|
|
|
7
8
|
|
|
8
9
|
program
|
|
9
10
|
.name('post-api-sync')
|
|
10
|
-
.description('Sync Postman and Insomnia collections from API code')
|
|
11
|
+
.description('Sync Postman and Insomnia collections from API code')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
// Default behavior: run sync
|
|
14
|
+
await syncOnce();
|
|
15
|
+
});
|
|
11
16
|
|
|
12
17
|
program
|
|
13
18
|
.command('init')
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "post-api-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Sync Postman and Insomnia collections from API code",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/EzekielMisgae/post-api-sync.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "commonjs",
|
|
6
10
|
"bin": {
|
|
7
11
|
"post-api-sync": "bin/post-api-sync.js"
|
|
@@ -16,6 +20,7 @@
|
|
|
16
20
|
"@babel/traverse": "^7.24.0",
|
|
17
21
|
"chokidar": "^3.6.0",
|
|
18
22
|
"commander": "^12.0.0",
|
|
23
|
+
"dotenv": "^17.2.4",
|
|
19
24
|
"fast-glob": "^3.3.2",
|
|
20
25
|
"fs-extra": "^11.2.0",
|
|
21
26
|
"inquirer": "^9.2.0",
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
const { nanoid } = require('nanoid');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const { toPostmanPath, splitPath, extractPathParams } = require('../utils');
|
|
3
4
|
|
|
4
5
|
function buildPostmanCollection(endpoints, config) {
|
|
5
6
|
const name = (config.output && config.output.postman && config.output.postman.collectionName) || 'API Collection';
|
|
6
|
-
const baseUrl = config.sources.baseUrl || 'http://localhost:3000';
|
|
7
|
-
const groupBy = (config.organization && config.organization.groupBy) || '
|
|
7
|
+
const baseUrl = (config.sources && config.sources.baseUrl) || 'http://localhost:3000';
|
|
8
|
+
const groupBy = (config.organization && config.organization.groupBy) || 'folder';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
let items;
|
|
11
|
+
if (groupBy === 'folder') {
|
|
12
|
+
items = buildFolderItems(endpoints);
|
|
13
|
+
} else {
|
|
14
|
+
items = buildTaggedItems(endpoints);
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
return {
|
|
12
18
|
info: {
|
|
@@ -19,6 +25,63 @@ function buildPostmanCollection(endpoints, config) {
|
|
|
19
25
|
};
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
function buildFolderItems(endpoints) {
|
|
29
|
+
const root = { item: [], _folders: {} };
|
|
30
|
+
|
|
31
|
+
for (const endpoint of endpoints) {
|
|
32
|
+
// Determine module name from file path
|
|
33
|
+
// e.g. /abs/path/to/src/modules/health/routes.ts -> Health
|
|
34
|
+
// e.g. /abs/path/to/src/modules/orders/routes/order.routes.ts -> Orders
|
|
35
|
+
let moduleName = 'General';
|
|
36
|
+
if (endpoint.filePath) {
|
|
37
|
+
const parts = endpoint.filePath.split(path.sep);
|
|
38
|
+
const filename = parts[parts.length - 1];
|
|
39
|
+
const parent = parts[parts.length - 2];
|
|
40
|
+
const grandparent = parts[parts.length - 3];
|
|
41
|
+
|
|
42
|
+
if (filename === 'routes.ts' || filename.endsWith('.routes.ts')) {
|
|
43
|
+
if (parent === 'routes' && grandparent) {
|
|
44
|
+
moduleName = capitalize(grandparent);
|
|
45
|
+
} else {
|
|
46
|
+
moduleName = capitalize(parent);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// Fallback: try to find a meaningful parent
|
|
50
|
+
// If parent is 'routes', go up one
|
|
51
|
+
if (parent === 'routes' && grandparent) {
|
|
52
|
+
moduleName = capitalize(grandparent);
|
|
53
|
+
} else {
|
|
54
|
+
moduleName = capitalize(parent);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!root._folders[moduleName]) {
|
|
60
|
+
const folder = { name: moduleName, item: [], _folders: {} };
|
|
61
|
+
root.item.push(folder);
|
|
62
|
+
root._folders[moduleName] = folder;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
root._folders[moduleName].item.push(buildItem(endpoint));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return cleanFolders(root.item);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function capitalize(str) {
|
|
72
|
+
if (!str) return str;
|
|
73
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cleanFolders(items) {
|
|
77
|
+
// Remove temporary _folders property
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
if (item._folders) delete item._folders;
|
|
80
|
+
if (item.item) cleanFolders(item.item);
|
|
81
|
+
}
|
|
82
|
+
return items;
|
|
83
|
+
}
|
|
84
|
+
|
|
22
85
|
function buildTaggedItems(endpoints) {
|
|
23
86
|
const groups = new Map();
|
|
24
87
|
for (const endpoint of endpoints) {
|
|
@@ -57,10 +120,10 @@ function buildItem(endpoint) {
|
|
|
57
120
|
},
|
|
58
121
|
body: hasBody
|
|
59
122
|
? {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
123
|
+
mode: 'raw',
|
|
124
|
+
raw: JSON.stringify(example || {}, null, 2),
|
|
125
|
+
options: { raw: { language: 'json' } }
|
|
126
|
+
}
|
|
64
127
|
: undefined
|
|
65
128
|
},
|
|
66
129
|
response: []
|
package/src/config.js
CHANGED
|
@@ -6,7 +6,7 @@ const ALWAYS_EXCLUDE = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*
|
|
|
6
6
|
const DEFAULT_CONFIG = {
|
|
7
7
|
framework: 'auto',
|
|
8
8
|
sources: {
|
|
9
|
-
include: ['src
|
|
9
|
+
include: ['src/**/routes.ts', 'src/**/*.routes.ts'],
|
|
10
10
|
exclude: ['**/*.spec.ts', '**/*.test.ts', 'node_modules/**', 'dist/**', 'build/**'],
|
|
11
11
|
baseUrl: 'http://localhost:3000'
|
|
12
12
|
},
|
|
@@ -28,7 +28,7 @@ const DEFAULT_CONFIG = {
|
|
|
28
28
|
markDeprecated: true
|
|
29
29
|
},
|
|
30
30
|
organization: {
|
|
31
|
-
groupBy: '
|
|
31
|
+
groupBy: 'folder'
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
package/src/merge/postman.js
CHANGED
|
@@ -57,32 +57,64 @@ function flattenPostmanItems(items, out = []) {
|
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function rebuildFromFlat(templateItems,
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
folderMap.set(folder.name, { name: folder.name, item: [] });
|
|
67
|
-
}
|
|
60
|
+
function rebuildFromFlat(templateItems, mergedItems) {
|
|
61
|
+
// Create a map of merged items for quick lookup
|
|
62
|
+
const mergedMap = new Map();
|
|
63
|
+
for (const item of mergedItems) {
|
|
64
|
+
const key = keyFromPostmanItem(item);
|
|
65
|
+
if (key) mergedMap.set(key, item);
|
|
68
66
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
|
|
68
|
+
// 1. Rebuild the structure based on the template (newly generated structure)
|
|
69
|
+
const rebuilt = replaceItemsRecursive(templateItems, mergedMap);
|
|
70
|
+
|
|
71
|
+
// 2. Identify items that are in mergedItems (existing+deprecated) but NOT in template
|
|
72
|
+
// These are effectively "extra" items (old manual items, or deprecated ones)
|
|
73
|
+
// We want to keep them, usually appended to the root
|
|
74
|
+
const usedKeys = new Set();
|
|
75
|
+
traverseKeys(rebuilt, usedKeys);
|
|
76
|
+
|
|
77
|
+
const leftovers = mergedItems.filter(item => {
|
|
78
|
+
const key = keyFromPostmanItem(item);
|
|
79
|
+
return key && !usedKeys.has(key);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Add leftovers to the root
|
|
83
|
+
return [...rebuilt, ...leftovers];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function replaceItemsRecursive(items, mergedMap) {
|
|
87
|
+
return items.map(item => {
|
|
88
|
+
if (item.item) {
|
|
89
|
+
// It's a folder
|
|
90
|
+
return { ...item, item: replaceItemsRecursive(item.item, mergedMap) };
|
|
91
|
+
}
|
|
92
|
+
// It's a request
|
|
93
|
+
const key = keyFromPostmanItem(item);
|
|
94
|
+
if (key && mergedMap.has(key)) {
|
|
95
|
+
// Return the merged version (which has description etc from existing)
|
|
96
|
+
// But we must preserve the new name/folder context if it changed?
|
|
97
|
+
// Actually mergedMap has the MERGED item.
|
|
98
|
+
// If the folder structure changed, 'item' here is from template, so it's in the right place.
|
|
99
|
+
// We just want the content from mergedMap.
|
|
100
|
+
return mergedMap.get(key);
|
|
101
|
+
}
|
|
102
|
+
return item;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function traverseKeys(items, keySet) {
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
if (item.item) {
|
|
109
|
+
traverseKeys(item.item, keySet);
|
|
73
110
|
} else {
|
|
74
|
-
|
|
75
|
-
|
|
111
|
+
const key = keyFromPostmanItem(item);
|
|
112
|
+
if (key) keySet.add(key);
|
|
76
113
|
}
|
|
77
114
|
}
|
|
78
|
-
return Array.from(folderMap.values());
|
|
79
115
|
}
|
|
80
116
|
|
|
81
|
-
|
|
82
|
-
const match = name.match(/^[A-Z]+\s+\S+/);
|
|
83
|
-
if (match) return 'General';
|
|
84
|
-
return 'General';
|
|
85
|
-
}
|
|
117
|
+
|
|
86
118
|
|
|
87
119
|
function keyFromPostmanItem(item) {
|
|
88
120
|
if (!item || !item.request) return null;
|
package/src/sync.js
CHANGED
|
@@ -13,9 +13,9 @@ async function syncOnce({ configPath, baseDir, postmanKey, postmanId } = {}) {
|
|
|
13
13
|
try {
|
|
14
14
|
const { config, baseDir: resolvedBase } = await loadConfig(configPath, baseDir);
|
|
15
15
|
const cwd = resolvedBase || process.cwd();
|
|
16
|
-
// Use CLI args or config values
|
|
17
|
-
const pmKey = postmanKey || (config.output && config.output.postman && config.output.postman.apiKey);
|
|
18
|
-
const pmId = postmanId || (config.output && config.output.postman && config.output.postman.collectionId);
|
|
16
|
+
// Use CLI args, env vars, or config values
|
|
17
|
+
const pmKey = postmanKey || process.env.POSTMAN_API_KEY || (config.output && config.output.postman && config.output.postman.apiKey);
|
|
18
|
+
const pmId = postmanId || process.env.POSTMAN_COLLECTION_ID || (config.output && config.output.postman && config.output.postman.collectionId);
|
|
19
19
|
|
|
20
20
|
const include = normalizeIncludePatterns(config.sources.include || [], cwd);
|
|
21
21
|
const exclude = Array.from(new Set([
|
|
@@ -60,6 +60,7 @@ async function syncOnce({ configPath, baseDir, postmanKey, postmanId } = {}) {
|
|
|
60
60
|
await fs.outputJson(outPath, merged, { spaces: 2 });
|
|
61
61
|
success(`Postman collection written to ${path.relative(process.cwd(), outPath)}`);
|
|
62
62
|
|
|
63
|
+
// Auto-sync checks
|
|
63
64
|
if (pmKey && pmId) {
|
|
64
65
|
info(`Pushing to Postman Cloud (ID: ${pmId})...`);
|
|
65
66
|
try {
|
|
@@ -68,6 +69,11 @@ async function syncOnce({ configPath, baseDir, postmanKey, postmanId } = {}) {
|
|
|
68
69
|
} catch (err) {
|
|
69
70
|
error(`Failed to sync to Postman Cloud: ${err.message}`);
|
|
70
71
|
}
|
|
72
|
+
} else if (pmKey || pmId) {
|
|
73
|
+
// Partial keys present
|
|
74
|
+
warn('Skipping Postman Cloud sync: Missing API Key or Collection ID.');
|
|
75
|
+
if (!pmKey) warn(' -> POSTMAN_API_KEY is missing');
|
|
76
|
+
if (!pmId) warn(' -> POSTMAN_COLLECTION_ID is missing');
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
package/src/watch.js
CHANGED
|
@@ -3,7 +3,7 @@ const { loadConfig, normalizeIncludePatterns, normalizeExcludePatterns, ALWAYS_E
|
|
|
3
3
|
const { syncOnce } = require('./sync');
|
|
4
4
|
const { info } = require('./log');
|
|
5
5
|
|
|
6
|
-
async function watchMode({ configPath, baseDir } = {}) {
|
|
6
|
+
async function watchMode({ configPath, baseDir, postmanKey, postmanId } = {}) {
|
|
7
7
|
const { config, baseDir: resolvedBase } = await loadConfig(configPath, baseDir);
|
|
8
8
|
const cwd = resolvedBase || process.cwd();
|
|
9
9
|
const include = normalizeIncludePatterns(config.sources.include || [], cwd);
|
|
@@ -19,7 +19,7 @@ async function watchMode({ configPath, baseDir } = {}) {
|
|
|
19
19
|
const trigger = () => {
|
|
20
20
|
if (timer) clearTimeout(timer);
|
|
21
21
|
timer = setTimeout(() => {
|
|
22
|
-
syncOnce({ configPath, baseDir: cwd });
|
|
22
|
+
syncOnce({ configPath, baseDir: cwd, postmanKey, postmanId });
|
|
23
23
|
}, debounceMs);
|
|
24
24
|
};
|
|
25
25
|
|