Convert Postman's simple dynamic variables to template functions (#680)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-15 13:48:27 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent c9990d8492
commit ae518dfb4c
6 changed files with 230 additions and 22 deletions
+21 -4
View File
@@ -30,6 +30,19 @@ const OAUTH1_SIGNATURE_METHODS: Record<string, string> = {
};
const DEFAULT_OAUTH1_SIGNATURE_METHOD = "HMAC-SHA1";
// Postman's dynamic variables that a Yaak template function reproduces exactly.
// The faker-backed ones ({{$randomFirstName}} and friends) have no equivalent and
// are left alone. Arguments have to be single-quoted; the template parser reads a
// double-quoted call as raw text.
const POSTMAN_DYNAMIC_VARIABLES: Record<string, string> = {
$guid: "uuid.v4()",
$randomUUID: "uuid.v4()",
$timestamp: "timestamp.unix()",
$isoTimestamp: "timestamp.iso8601()",
// Postman's $randomInt is an integer from 0 to 1000
$randomInt: "random.range(min='0',max='1000',decimals='0')",
};
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
@@ -604,10 +617,14 @@ function importDescription(rawDescription: unknown): string | undefined {
/** 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().replace(/^vault:/, "")}]}`,
) as T;
return obj.replace(/{{\s*(_\.)?([^}]*)\s*}}/g, (_m, _dot, expr) => {
const name = String(expr).trim();
// hasOwn, so a collection using {{constructor}} doesn't reach Object.prototype
if (Object.hasOwn(POSTMAN_DYNAMIC_VARIABLES, name)) {
return `\${[${POSTMAN_DYNAMIC_VARIABLES[name]}]}`;
}
return `\${[${name.replace(/^vault:/, "")}]}`;
}) as T;
}
if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
@@ -0,0 +1,67 @@
{
"info": {
"_postman_id": "5c0ffee0-dyn0-4var-9abc-000000000001",
"name": "Dynamic Variables",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Dynamic Variables",
"request": {
"method": "POST",
"header": [
{
"key": "X-Request-Id",
"value": "{{$guid}}"
},
{
"key": "X-Correlation-Id",
"value": "{{$randomUUID}}"
},
{
"key": "X-Sent-At",
"value": "{{$timestamp}}"
},
{
"key": "X-Sent-At-Iso",
"value": "{{$isoTimestamp}}"
},
{
"key": "X-Spaced",
"value": "{{ $guid }}"
},
{
"key": "X-Faker",
"value": "{{$randomFirstName}}"
},
{
"key": "X-Plain",
"value": "{{base_url}}"
}
],
"url": {
"raw": "https://yaak.app/x/echo?n={{$randomInt}}",
"protocol": "https",
"host": ["yaak", "app"],
"path": ["x", "echo"],
"query": [
{
"key": "n",
"value": "{{$randomInt}}"
}
]
},
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"{{$guid}}\",\n \"at\": {{$timestamp}},\n \"mixed\": \"{{$guid}}/{{base_url}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
}
@@ -0,0 +1,95 @@
{
"resources": {
"workspaces": [
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_1",
"name": "Dynamic Variables",
"authenticationType": null,
"authentication": {}
}
],
"environments": [
{
"model": "environment",
"id": "GENERATE_ID::ENVIRONMENT_1",
"name": "Global Variables",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"parentModel": "workspace",
"parentId": null,
"variables": []
}
],
"httpRequests": [
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_15",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"folderId": null,
"name": "Dynamic Variables",
"method": "POST",
"url": "https://yaak.app/x/echo",
"urlParameters": [
{
"name": "n",
"value": "${[random.range(min='0',max='1000',decimals='0')]}",
"enabled": true
}
],
"body": {
"text": "{\n \"id\": \"${[uuid.v4()]}\",\n \"at\": ${[timestamp.unix()]},\n \"mixed\": \"${[uuid.v4()]}/${[base_url]}\"\n}"
},
"bodyType": "application/json",
"sortPriority": 0,
"headers": [
{
"name": "X-Request-Id",
"value": "${[uuid.v4()]}",
"enabled": true
},
{
"name": "X-Correlation-Id",
"value": "${[uuid.v4()]}",
"enabled": true
},
{
"name": "X-Sent-At",
"value": "${[timestamp.unix()]}",
"enabled": true
},
{
"name": "X-Sent-At-Iso",
"value": "${[timestamp.iso8601()]}",
"enabled": true
},
{
"name": "X-Spaced",
"value": "${[uuid.v4()]}",
"enabled": true
},
{
"name": "X-Faker",
"value": "${[$randomFirstName]}",
"enabled": true
},
{
"name": "X-Plain",
"value": "${[base_url]}",
"enabled": true
},
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"authenticationType": null,
"authentication": {}
}
],
"folders": []
},
"sourceKeys": {
"GENERATE_ID::WORKSPACE_1": "collection:5c0ffee0-dyn0-4var-9abc-000000000001"
}
}
+12 -12
View File
@@ -3,7 +3,7 @@
"workspaces": [
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_1",
"id": "GENERATE_ID::WORKSPACE_2",
"name": "New Collection",
"authenticationType": null,
"authentication": {}
@@ -12,9 +12,9 @@
"environments": [
{
"model": "environment",
"id": "GENERATE_ID::ENVIRONMENT_1",
"id": "GENERATE_ID::ENVIRONMENT_2",
"name": "Global Variables",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"parentModel": "workspace",
"parentId": null,
"variables": []
@@ -23,8 +23,8 @@
"httpRequests": [
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_15",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"id": "GENERATE_ID::HTTP_REQUEST_16",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"folderId": "GENERATE_ID::FOLDER_2",
"name": "Request 1",
"method": "GET",
@@ -39,8 +39,8 @@
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_16",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"id": "GENERATE_ID::HTTP_REQUEST_17",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"folderId": "GENERATE_ID::FOLDER_1",
"name": "Request 2",
"method": "GET",
@@ -55,8 +55,8 @@
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_17",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"id": "GENERATE_ID::HTTP_REQUEST_18",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"folderId": null,
"name": "Request 3",
"method": "GET",
@@ -74,7 +74,7 @@
{
"model": "folder",
"sortPriority": 0,
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"id": "GENERATE_ID::FOLDER_1",
"name": "Top Folder",
"folderId": null,
@@ -84,7 +84,7 @@
{
"model": "folder",
"sortPriority": 1,
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"id": "GENERATE_ID::FOLDER_2",
"name": "Nested Folder",
"folderId": "GENERATE_ID::FOLDER_1",
@@ -94,6 +94,6 @@
]
},
"sourceKeys": {
"GENERATE_ID::WORKSPACE_1": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
"GENERATE_ID::WORKSPACE_2": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
}
}
+6 -6
View File
@@ -3,7 +3,7 @@
"workspaces": [
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_2",
"id": "GENERATE_ID::WORKSPACE_3",
"name": "New Collection",
"authenticationType": "basic",
"authentication": {
@@ -15,9 +15,9 @@
"environments": [
{
"model": "environment",
"id": "GENERATE_ID::ENVIRONMENT_2",
"id": "GENERATE_ID::ENVIRONMENT_3",
"name": "Global Variables",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"workspaceId": "GENERATE_ID::WORKSPACE_3",
"parentModel": "workspace",
"parentId": null,
"variables": [
@@ -31,8 +31,8 @@
"httpRequests": [
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_18",
"workspaceId": "GENERATE_ID::WORKSPACE_2",
"id": "GENERATE_ID::HTTP_REQUEST_19",
"workspaceId": "GENERATE_ID::WORKSPACE_3",
"folderId": null,
"name": "Form URL",
"method": "POST",
@@ -102,6 +102,6 @@
"folders": []
},
"sourceKeys": {
"GENERATE_ID::WORKSPACE_2": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
"GENERATE_ID::WORKSPACE_3": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
}
}
@@ -154,6 +154,35 @@ describe("importer-postman", () => {
]);
});
test("Leaves {{constructor}} alone instead of reaching Object.prototype", () => {
const result = convertPostman(
JSON.stringify({
info: {
name: "Prototype Key",
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
},
item: [
{
name: "Request",
request: {
method: "GET",
url: "https://yaak.app",
header: [
{ key: "X-A", value: "{{constructor}}" },
{ key: "X-B", value: "{{toString}}" },
],
},
},
],
}),
);
expect(result?.resources.httpRequests[0]?.headers).toEqual([
{ name: "X-A", value: "${[constructor]}", enabled: true },
{ name: "X-B", value: "${[toString]}", enabled: true },
]);
});
test("Omits keys for items the collection never identified", () => {
const result = convertPostman(
JSON.stringify({