Import postman environments

https://feedback.yaak.app/p/import-postman-environments
This commit is contained in:
Gregory Schier
2025-10-21 07:20:37 -07:00
parent 25b110778a
commit 0520ef5d43
14 changed files with 421 additions and 191 deletions

View File

@@ -0,0 +1,14 @@
{
"name": "@yaak/importer-postman-environment",
"displayName": "Postman Environment Importer",
"description": "Import environments from Postman",
"private": true,
"version": "0.1.0",
"main": "./build/index.js",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -0,0 +1,138 @@
import type {
Context,
Environment,
PartialImportResources,
PluginDefinition,
Workspace,
} from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export const plugin: PluginDefinition = {
importer: {
name: 'Postman Environment',
description: 'Import postman environment exports',
onImport(_ctx: Context, args: { text: string }) {
return convertPostmanEnvironment(args.text);
},
},
};
export function convertPostmanEnvironment(contents: string): ImportPluginResponse | undefined {
const root = parseJSONToRecord(contents);
if (root == null) return;
// Validate that it looks like a Postman Environment export
const values = toArray<{
key?: string;
value?: unknown;
enabled?: boolean;
description?: string;
type?: string;
}>(root.values);
const scope = root._postman_variable_scope;
const hasEnvMarkers = typeof scope === 'string';
if (values.length === 0 || (!hasEnvMarkers && typeof root.name !== 'string')) {
// Not a Postman environment file, skip
return;
}
const exportResources: ExportResources = {
workspaces: [],
environments: [],
};
const envVariables = values
.map((v) => ({
enabled: v.enabled ?? true,
name: String(v.key ?? ''),
value: String(v.value),
description: v.description ? String(v.description) : null,
}))
.filter((v) => v.name.length > 0);
const environment: ExportResources['environments'][0] = {
model: 'environment',
id: generateId('environment'),
name: root.name ? String(root.name) : 'Environment',
workspaceId: 'CURRENT_WORKSPACE',
parentModel: 'environment',
parentId: null,
variables: envVariables,
};
exportResources.environments.push(environment);
const resources = deleteUndefinedAttrs(
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources };
}
function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
try {
return toRecord(JSON.parse(jsonStr));
} catch {
return null;
}
}
function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, T>;
}
return {} as Record<string, T>;
}
function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return [] as T[];
}
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(
/{{\s*(_\.)?([^}]*)\s*}}/g,
(_m, _dot, expr) => '${[' + expr.trim() + ']}',
) as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
} else {
return obj;
}
}
function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
}
const idCount: Partial<Record<string, number>> = {};
function generateId(model: string): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
export default plugin;

View File

@@ -0,0 +1,27 @@
{
"id": "123",
"name": "My Environment",
"values": [
{
"key": "baseUrl",
"value": "https://api.example.com",
"type": "default",
"enabled": true
},
{
"key": "token",
"value": "{{ access_token }}",
"type": "default",
"description": "Access token for the API.",
"enabled": true
},
{
"key": "disabled",
"type": "secret",
"value": "hello",
"enabled": false
}
],
"_postman_variable_scope": "environment",
"_postman_exported_using": "PostmanRuntime/1.0.0"
}

View File

@@ -0,0 +1,34 @@
{
"resources": {
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_0",
"model": "environment",
"name": "My Environment",
"variables": [
{
"enabled": true,
"description": null,
"name": "baseUrl",
"value": "https://api.example.com"
},
{
"enabled": true,
"description": "Access token for the API.",
"name": "token",
"value": "${[access_token]}"
},
{
"enabled": false,
"description": null,
"name": "disabled",
"value": "hello"
}
],
"workspaceId": "CURRENT_WORKSPACE",
"parentId": null,
"parentModel": "environment"
}
]
}
}

View File

@@ -0,0 +1,22 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import { convertPostmanEnvironment } from '../src';
describe('importer-postman-environment', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
if (fixture.includes('.output')) {
continue;
}
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
const result = convertPostmanEnvironment(contents);
expect(result).toEqual(JSON.parse(expected));
});
}
});

View File

@@ -376,7 +376,7 @@ function toArray<T>(value: unknown): T[] {
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
return obj.replace(/{{\s*(_\.)?([^}]*)\s*}}/g, (_m, _dot, expr) => '${[' + expr.trim() + ']}') as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {