feat(import): add stable per-resource source keys to the importer contract (#614)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-01 08:11:54 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent e63a87718c
commit 81a2a5d955
28 changed files with 785 additions and 49 deletions
@@ -959,6 +959,10 @@ describe("importer-curl", () => {
{ enabled: true, name: "q", value: "a=b" },
]);
});
test("Emits no source keys", () => {
expect(convertCurl("curl https://yaak.app")).not.toHaveProperty("sourceKeys");
});
});
const idCount: Partial<Record<string, number>> = {};
+15
View File
@@ -15,6 +15,21 @@ export function convertId(id: string): string {
return `GENERATE_ID::${id}`;
}
export function createSourceKeys() {
const keys: Record<string, string> = {};
return {
/** Convert a resource's own document ID, keeping it as that resource's source key. */
own(id: string): string {
const converted = convertId(id);
keys[converted] = id;
return converted;
},
all: (): Record<string, string> => keys,
};
}
export type SourceKeys = ReturnType<typeof createSourceKeys>;
export function importHttpBodyAndHeaders(obj: any) {
const { headers } = importHeaders(obj);
const { body, bodyType } = importHttpBody(obj.body);
+35 -14
View File
@@ -1,10 +1,18 @@
/* oxlint-disable no-explicit-any */
import type { PartialImportResources } from "@yaakapp/api";
import { convertId, convertTemplateSyntax, importHttpBodyAndHeaders, isJSObject } from "./common";
import {
convertId,
convertTemplateSyntax,
createSourceKeys,
importHttpBodyAndHeaders,
isJSObject,
type SourceKeys,
} from "./common";
export function convertInsomniaV4(parsed: any) {
if (!Array.isArray(parsed.resources)) return null;
const keys = createSourceKeys();
const resources: PartialImportResources = {
environments: [],
folders: [],
@@ -20,7 +28,7 @@ export function convertInsomniaV4(parsed: any) {
);
for (const w of workspacesToImport) {
resources.workspaces.push({
id: convertId(w._id),
id: keys.own(w._id),
createdAt: w.created ? new Date(w.created).toISOString().replace("Z", "") : undefined,
updatedAt: w.updated ? new Date(w.updated).toISOString().replace("Z", "") : undefined,
model: "workspace",
@@ -31,7 +39,7 @@ export function convertInsomniaV4(parsed: any) {
(r: any) => isJSObject(r) && r._type === "environment",
);
resources.environments.push(
...environmentsToImport.map((r: any) => importEnvironment(r, w._id)),
...environmentsToImport.map((r: any) => importEnvironment(r, w._id, keys)),
);
const nextFolder = (parentId: string) => {
@@ -40,12 +48,12 @@ export function convertInsomniaV4(parsed: any) {
if (!isJSObject(child)) continue;
if (child._type === "request_group") {
resources.folders.push(importFolder(child, w._id));
resources.folders.push(importFolder(child, w._id, keys));
nextFolder(child._id);
} else if (child._type === "request") {
resources.httpRequests.push(importHttpRequest(child, w._id));
resources.httpRequests.push(importHttpRequest(child, w._id, keys));
} else if (child._type === "grpc_request") {
resources.grpcRequests.push(importGrpcRequest(child, w._id));
resources.grpcRequests.push(importGrpcRequest(child, w._id, keys));
}
}
};
@@ -60,10 +68,14 @@ export function convertInsomniaV4(parsed: any) {
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources: convertTemplateSyntax(resources) };
return { resources: convertTemplateSyntax(resources), sourceKeys: keys.all() };
}
function importHttpRequest(r: any, workspaceId: string): PartialImportResources["httpRequests"][0] {
function importHttpRequest(
r: any,
workspaceId: string,
keys: SourceKeys,
): PartialImportResources["httpRequests"][0] {
let authenticationType: string | null = null;
let authentication = {};
if (r.authentication.type === "bearer") {
@@ -80,7 +92,7 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
}
return {
id: convertId(r.meta?.id ?? r._id),
id: keys.own(r.meta?.id ?? r._id),
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : undefined,
updatedAt: r.modified ? new Date(r.modified).toISOString().replace("Z", "") : undefined,
workspaceId: convertId(workspaceId),
@@ -102,13 +114,17 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
};
}
function importGrpcRequest(r: any, workspaceId: string): PartialImportResources["grpcRequests"][0] {
function importGrpcRequest(
r: any,
workspaceId: string,
keys: SourceKeys,
): PartialImportResources["grpcRequests"][0] {
const parts = r.protoMethodName.split("/").filter((p: any) => p !== "");
const service = parts[0] ?? null;
const method = parts[1] ?? null;
return {
id: convertId(r.meta?.id ?? r._id),
id: keys.own(r.meta?.id ?? r._id),
createdAt: r.created ? new Date(r.created).toISOString().replace("Z", "") : undefined,
updatedAt: r.modified ? new Date(r.modified).toISOString().replace("Z", "") : undefined,
workspaceId: convertId(workspaceId),
@@ -131,9 +147,13 @@ function importGrpcRequest(r: any, workspaceId: string): PartialImportResources[
};
}
function importFolder(f: any, workspaceId: string): PartialImportResources["folders"][0] {
function importFolder(
f: any,
workspaceId: string,
keys: SourceKeys,
): PartialImportResources["folders"][0] {
return {
id: convertId(f._id),
id: keys.own(f._id),
createdAt: f.created ? new Date(f.created).toISOString().replace("Z", "") : undefined,
updatedAt: f.modified ? new Date(f.modified).toISOString().replace("Z", "") : undefined,
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
@@ -147,11 +167,12 @@ function importFolder(f: any, workspaceId: string): PartialImportResources["fold
function importEnvironment(
e: any,
workspaceId: string,
keys: SourceKeys,
isParentOg?: boolean,
): PartialImportResources["environments"][0] {
const isParent = isParentOg ?? e.parentId === workspaceId;
return {
id: convertId(e._id),
id: keys.own(e._id),
createdAt: e.created ? new Date(e.created).toISOString().replace("Z", "") : undefined,
updatedAt: e.modified ? new Date(e.modified).toISOString().replace("Z", "") : undefined,
workspaceId: convertId(workspaceId),
+24 -14
View File
@@ -3,9 +3,11 @@ import type { PartialImportResources } from "@yaakapp/api";
import {
convertId,
convertTemplateSyntax,
createSourceKeys,
importHeaders,
importHttpBodyAndHeaders,
isJSObject,
type SourceKeys,
} from "./common";
export function convertInsomniaV5(parsed: any) {
@@ -18,6 +20,7 @@ export function convertInsomniaV5(parsed: any) {
return null;
}
const keys = createSourceKeys();
const resources: PartialImportResources = {
environments: [],
folders: [],
@@ -30,7 +33,7 @@ export function convertInsomniaV5(parsed: any) {
// Import workspaces
const meta = ("meta" in parsed ? parsed.meta : {}) as Record<string, any>;
resources.workspaces.push({
id: convertId(meta.id ?? "collection"),
id: keys.own(meta.id ?? "collection"),
createdAt: meta.created ? new Date(meta.created).toISOString().replace("Z", "") : undefined,
updatedAt: meta.modified ? new Date(meta.modified).toISOString().replace("Z", "") : undefined,
model: "workspace",
@@ -42,8 +45,10 @@ export function convertInsomniaV5(parsed: any) {
// Import environments
resources.environments.push(
importEnvironment(parsed.environments, meta.id, true),
...(parsed.environments.subEnvironments ?? []).map((r: any) => importEnvironment(r, meta.id)),
importEnvironment(parsed.environments, meta.id, keys, true),
...(parsed.environments.subEnvironments ?? []).map((r: any) =>
importEnvironment(r, meta.id, keys),
),
);
// Import folders
@@ -52,16 +57,16 @@ export function convertInsomniaV5(parsed: any) {
if (!isJSObject(child)) continue;
if (Array.isArray(child.children)) {
const { folder, environment } = importFolder(child, meta.id, parentId);
const { folder, environment } = importFolder(child, meta.id, parentId, keys);
resources.folders.push(folder);
if (environment) resources.environments.push(environment);
nextFolder(child.children, child.meta.id);
} else if (child.method) {
resources.httpRequests.push(importHttpRequest(child, meta.id, parentId));
resources.httpRequests.push(importHttpRequest(child, meta.id, parentId, keys));
} else if (child.protoFileId) {
resources.grpcRequests.push(importGrpcRequest(child, meta.id, parentId));
resources.grpcRequests.push(importGrpcRequest(child, meta.id, parentId, keys));
} else if (child.url) {
resources.websocketRequests.push(importWebsocketRequest(child, meta.id, parentId));
resources.websocketRequests.push(importWebsocketRequest(child, meta.id, parentId, keys));
}
}
};
@@ -75,13 +80,14 @@ export function convertInsomniaV5(parsed: any) {
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources: convertTemplateSyntax(resources) };
return { resources: convertTemplateSyntax(resources), sourceKeys: keys.all() };
}
function importHttpRequest(
r: any,
workspaceId: string,
parentId: string,
keys: SourceKeys,
): PartialImportResources["httpRequests"][0] {
const id = r.meta?.id ?? r._id;
const created = r.meta?.created ?? r.created;
@@ -89,7 +95,7 @@ function importHttpRequest(
const sortKey = r.meta?.sortKey ?? r.sortKey;
return {
id: convertId(id),
id: keys.own(id),
workspaceId: convertId(workspaceId),
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
@@ -114,6 +120,7 @@ function importGrpcRequest(
r: any,
workspaceId: string,
parentId: string,
keys: SourceKeys,
): PartialImportResources["grpcRequests"][0] {
const id = r.meta?.id ?? r._id;
const created = r.meta?.created ?? r.created;
@@ -126,7 +133,7 @@ function importGrpcRequest(
return {
model: "grpc_request",
id: convertId(id),
id: keys.own(id),
workspaceId: convertId(workspaceId),
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
@@ -152,6 +159,7 @@ function importWebsocketRequest(
r: any,
workspaceId: string,
parentId: string,
keys: SourceKeys,
): PartialImportResources["websocketRequests"][0] {
const id = r.meta?.id ?? r._id;
const created = r.meta?.created ?? r.created;
@@ -160,7 +168,7 @@ function importWebsocketRequest(
return {
model: "websocket_request",
id: convertId(id),
id: keys.own(id),
workspaceId: convertId(workspaceId),
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
@@ -198,6 +206,7 @@ function importFolder(
f: any,
workspaceId: string,
parentId: string,
keys: SourceKeys,
): {
folder: PartialImportResources["folders"][0];
environment: PartialImportResources["environments"][0] | null;
@@ -210,7 +219,7 @@ function importFolder(
let environment: PartialImportResources["environments"][0] | null = null;
if (Object.keys(f.environment ?? {}).length > 0) {
environment = {
id: convertId(`${id}folder`),
id: keys.own(`${id}folder`),
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
workspaceId: convertId(workspaceId),
@@ -230,7 +239,7 @@ function importFolder(
return {
folder: {
model: "folder",
id: convertId(id),
id: keys.own(id),
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
@@ -248,6 +257,7 @@ function importFolder(
function importEnvironment(
e: any,
workspaceId: string,
keys: SourceKeys,
isParent?: boolean,
): PartialImportResources["environments"][0] {
const id = e.meta?.id ?? e._id;
@@ -256,7 +266,7 @@ function importEnvironment(
const sortKey = e.meta?.sortKey ?? e.sortKey;
return {
id: convertId(id),
id: keys.own(id),
createdAt: created ? new Date(created).toISOString().replace("Z", "") : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace("Z", "") : undefined,
workspaceId: convertId(workspaceId),
@@ -132,5 +132,13 @@
"name": "Dummy"
}
]
},
"sourceKeys": {
"GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d": "env_799ae3d723ef44af91b4817e5d057e6d",
"GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553": "env_030fbfdbb274426ebd78e2e6518f8553",
"GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3": "fld_859d1df78261463480b6a3a1419517e3",
"GENERATE_ID::req_84cd9ae4bd034dd8bb730e856a665cbb": "req_84cd9ae4bd034dd8bb730e856a665cbb",
"GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019": "wrk_d4d92f7c0ee947b89159243506687019"
}
}
@@ -116,5 +116,13 @@
"headers": []
}
]
},
"sourceKeys": {
"GENERATE_ID::env_e46dc73e8ccda30ca132153e8f11183bd08119ce": "env_e46dc73e8ccda30ca132153e8f11183bd08119ce",
"GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7folder": "fld_296933ea4ea84783a775d199997e9be7folder",
"GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7": "fld_296933ea4ea84783a775d199997e9be7",
"GENERATE_ID::req_9a80320365ac4509ade406359dbc6a71": "req_9a80320365ac4509ade406359dbc6a71",
"GENERATE_ID::req_e3f8cdbd58784a539dd4c1e127d73451": "req_e3f8cdbd58784a539dd4c1e127d73451",
"GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c": "wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
}
}
@@ -189,5 +189,15 @@
"headers": []
}
]
},
"sourceKeys": {
"GENERATE_ID::env_20945044d3c8497ca8b717bef750987e": "env_20945044d3c8497ca8b717bef750987e",
"GENERATE_ID::env_6f7728bb7fc04d558d668e954d756ea2": "env_6f7728bb7fc04d558d668e954d756ea2",
"GENERATE_ID::env_976a8b6eb5d44fb6a20150f65c32d243": "env_976a8b6eb5d44fb6a20150f65c32d243",
"GENERATE_ID::fld_42eb2e2bb22b4cedacbd3d057634e80c": "fld_42eb2e2bb22b4cedacbd3d057634e80c",
"GENERATE_ID::greq_06d659324df94504a4d64632be7106b3": "greq_06d659324df94504a4d64632be7106b3",
"GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785": "req_d72fff2a6b104b91a2ebe9de9edd2785",
"GENERATE_ID::ws-req_5d1a4c7c79494743962e5176f6add270": "ws-req_5d1a4c7c79494743962e5176f6add270",
"GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53": "wrk_c1eacfa750a04f3ea9985ef28043fa53"
}
}
@@ -24,6 +24,38 @@ describe("importer-yaak", () => {
expect(result).toEqual(parseJsonOrYaml(expected));
});
}
test("Keys resources by their Insomnia _id, unchanged by a rename", () => {
const collection = (requestName: string) =>
YAML.stringify({
type: "collection.insomnia.rest/5.0",
name: "Keys",
meta: { id: "wrk_1" },
environments: { meta: { id: "env_1" }, name: "Base", data: {} },
collection: [
{
meta: { id: "fld_1" },
name: "Folder",
children: [
{
meta: { id: "req_1" },
name: requestName,
method: "GET",
url: "https://yaak.app",
},
],
},
],
});
const before = convertInsomnia(collection("Original"));
const after = convertInsomnia(collection("Renamed"));
expect(before?.sourceKeys?.[before.resources.httpRequests[0]!.id]).toBe("req_1");
expect(after?.sourceKeys?.[after.resources.httpRequests[0]!.id]).toBe("req_1");
expect(before?.sourceKeys?.[before.resources.folders[0]!.id]).toBe("fld_1");
expect(before?.sourceKeys?.[before.resources.workspaces[0]!.id]).toBe("wrk_1");
});
});
function parseJsonOrYaml(text: string): unknown {
+22
View File
@@ -110,6 +110,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
const folderIdsByTag = new Map<string, string>();
const routeLabels = new Map<string, string>();
const sourceKeys: Record<string, string> = {};
for (const tag of toArray(spec.tags)) {
const tagRecord = toRecord(tag);
const name = stringAt(tagRecord, "name");
@@ -126,6 +127,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
};
resources.folders.push(folder);
folderIdsByTag.set(name, folder.id);
sourceKeys[folder.id] = tagSourceKey(name);
}
for (const [rawPath, rawPathItem] of Object.entries(toRecord(spec.paths))) {
@@ -139,6 +141,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
importState,
operation,
resources,
sourceKeys,
workspaceId: workspace.id,
});
@@ -160,6 +163,11 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
authenticationVariables,
});
routeLabels.set(request.id, `${method.toUpperCase()} ${rawPath}`);
sourceKeys[request.id] = operationSourceKey(
stringAt(operation, "operationId"),
method,
rawPath,
);
resources.httpRequests.push(request);
}
}
@@ -241,6 +249,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
websocketRequests: [],
workspaces: resources.workspaces,
}) as PartialImportResources,
sourceKeys,
};
}
@@ -681,12 +690,14 @@ function findOrCreateFolderId({
importState,
operation,
resources,
sourceKeys,
workspaceId,
}: {
folderIdsByTag: Map<string, string>;
importState: ImportState;
operation: UnknownRecord;
resources: ImportResources;
sourceKeys: Record<string, string>;
workspaceId: string;
}): string | null {
const tag = toArray(operation.tags).find((t): t is string => typeof t === "string");
@@ -705,9 +716,20 @@ function findOrCreateFolderId({
};
resources.folders.push(folder);
folderIdsByTag.set(tag, folder.id);
sourceKeys[folder.id] = tagSourceKey(tag);
return folder.id;
}
function operationSourceKey(operationId: string | undefined, method: string, path: string): string {
return operationId != null && operationId !== ""
? `op:${operationId}`
: `route:${method.toUpperCase()} ${path}`;
}
function tagSourceKey(tag: string): string {
return `tag:${tag}`;
}
/**
* Yaak's `:name` placeholders only substitute when they span a whole path
* segment and hold a single plain value. Templates elsewhere in a segment
@@ -308,6 +308,16 @@ License: CC0 1.0 (https://github.com/APIs-guru/openapi-directory#licenses)",
},
],
},
"sourceKeys": {
"GENERATE_ID::FOLDER_0": "tag:APIs",
"GENERATE_ID::HTTP_REQUEST_0": "op:listAPIs",
"GENERATE_ID::HTTP_REQUEST_1": "op:getMetrics",
"GENERATE_ID::HTTP_REQUEST_2": "op:getProviders",
"GENERATE_ID::HTTP_REQUEST_3": "op:getAPI",
"GENERATE_ID::HTTP_REQUEST_4": "op:getServiceAPI",
"GENERATE_ID::HTTP_REQUEST_5": "op:getProvider",
"GENERATE_ID::HTTP_REQUEST_6": "op:getServices",
},
}
`;
@@ -2600,6 +2610,97 @@ Contact: me@kennethreitz.org",
},
],
},
"sourceKeys": {
"GENERATE_ID::FOLDER_0": "tag:HTTP Methods",
"GENERATE_ID::FOLDER_1": "tag:Auth",
"GENERATE_ID::FOLDER_10": "tag:Anything",
"GENERATE_ID::FOLDER_2": "tag:Status codes",
"GENERATE_ID::FOLDER_3": "tag:Request inspection",
"GENERATE_ID::FOLDER_4": "tag:Response inspection",
"GENERATE_ID::FOLDER_5": "tag:Response formats",
"GENERATE_ID::FOLDER_6": "tag:Dynamic data",
"GENERATE_ID::FOLDER_7": "tag:Cookies",
"GENERATE_ID::FOLDER_8": "tag:Images",
"GENERATE_ID::FOLDER_9": "tag:Redirects",
"GENERATE_ID::HTTP_REQUEST_0": "route:GET /absolute-redirect/{n}",
"GENERATE_ID::HTTP_REQUEST_1": "route:DELETE /anything",
"GENERATE_ID::HTTP_REQUEST_10": "route:POST /anything/{anything}",
"GENERATE_ID::HTTP_REQUEST_11": "route:PUT /anything/{anything}",
"GENERATE_ID::HTTP_REQUEST_12": "route:TRACE /anything/{anything}",
"GENERATE_ID::HTTP_REQUEST_13": "route:GET /base64/{value}",
"GENERATE_ID::HTTP_REQUEST_14": "route:GET /basic-auth/{user}/{passwd}",
"GENERATE_ID::HTTP_REQUEST_15": "route:GET /bearer",
"GENERATE_ID::HTTP_REQUEST_16": "route:GET /brotli",
"GENERATE_ID::HTTP_REQUEST_17": "route:GET /bytes/{n}",
"GENERATE_ID::HTTP_REQUEST_18": "route:GET /cache",
"GENERATE_ID::HTTP_REQUEST_19": "route:GET /cache/{value}",
"GENERATE_ID::HTTP_REQUEST_2": "route:GET /anything",
"GENERATE_ID::HTTP_REQUEST_20": "route:GET /cookies",
"GENERATE_ID::HTTP_REQUEST_21": "route:GET /cookies/delete",
"GENERATE_ID::HTTP_REQUEST_22": "route:GET /cookies/set",
"GENERATE_ID::HTTP_REQUEST_23": "route:GET /cookies/set/{name}/{value}",
"GENERATE_ID::HTTP_REQUEST_24": "route:GET /deflate",
"GENERATE_ID::HTTP_REQUEST_25": "route:DELETE /delay/{delay}",
"GENERATE_ID::HTTP_REQUEST_26": "route:GET /delay/{delay}",
"GENERATE_ID::HTTP_REQUEST_27": "route:PATCH /delay/{delay}",
"GENERATE_ID::HTTP_REQUEST_28": "route:POST /delay/{delay}",
"GENERATE_ID::HTTP_REQUEST_29": "route:PUT /delay/{delay}",
"GENERATE_ID::HTTP_REQUEST_3": "route:PATCH /anything",
"GENERATE_ID::HTTP_REQUEST_30": "route:TRACE /delay/{delay}",
"GENERATE_ID::HTTP_REQUEST_31": "route:DELETE /delete",
"GENERATE_ID::HTTP_REQUEST_32": "route:GET /deny",
"GENERATE_ID::HTTP_REQUEST_33": "route:GET /digest-auth/{qop}/{user}/{passwd}",
"GENERATE_ID::HTTP_REQUEST_34": "route:GET /digest-auth/{qop}/{user}/{passwd}/{algorithm}",
"GENERATE_ID::HTTP_REQUEST_35": "route:GET /digest-auth/{qop}/{user}/{passwd}/{algorithm}/{stale_after}",
"GENERATE_ID::HTTP_REQUEST_36": "route:GET /drip",
"GENERATE_ID::HTTP_REQUEST_37": "route:GET /encoding/utf8",
"GENERATE_ID::HTTP_REQUEST_38": "route:GET /etag/{etag}",
"GENERATE_ID::HTTP_REQUEST_39": "route:GET /get",
"GENERATE_ID::HTTP_REQUEST_4": "route:POST /anything",
"GENERATE_ID::HTTP_REQUEST_40": "route:GET /gzip",
"GENERATE_ID::HTTP_REQUEST_41": "route:GET /headers",
"GENERATE_ID::HTTP_REQUEST_42": "route:GET /hidden-basic-auth/{user}/{passwd}",
"GENERATE_ID::HTTP_REQUEST_43": "route:GET /html",
"GENERATE_ID::HTTP_REQUEST_44": "route:GET /image",
"GENERATE_ID::HTTP_REQUEST_45": "route:GET /image/jpeg",
"GENERATE_ID::HTTP_REQUEST_46": "route:GET /image/png",
"GENERATE_ID::HTTP_REQUEST_47": "route:GET /image/svg",
"GENERATE_ID::HTTP_REQUEST_48": "route:GET /image/webp",
"GENERATE_ID::HTTP_REQUEST_49": "route:GET /ip",
"GENERATE_ID::HTTP_REQUEST_5": "route:PUT /anything",
"GENERATE_ID::HTTP_REQUEST_50": "route:GET /json",
"GENERATE_ID::HTTP_REQUEST_51": "route:GET /links/{n}/{offset}",
"GENERATE_ID::HTTP_REQUEST_52": "route:PATCH /patch",
"GENERATE_ID::HTTP_REQUEST_53": "route:POST /post",
"GENERATE_ID::HTTP_REQUEST_54": "route:PUT /put",
"GENERATE_ID::HTTP_REQUEST_55": "route:GET /range/{numbytes}",
"GENERATE_ID::HTTP_REQUEST_56": "route:DELETE /redirect-to",
"GENERATE_ID::HTTP_REQUEST_57": "route:GET /redirect-to",
"GENERATE_ID::HTTP_REQUEST_58": "route:PATCH /redirect-to",
"GENERATE_ID::HTTP_REQUEST_59": "route:POST /redirect-to",
"GENERATE_ID::HTTP_REQUEST_6": "route:TRACE /anything",
"GENERATE_ID::HTTP_REQUEST_60": "route:PUT /redirect-to",
"GENERATE_ID::HTTP_REQUEST_61": "route:TRACE /redirect-to",
"GENERATE_ID::HTTP_REQUEST_62": "route:GET /redirect/{n}",
"GENERATE_ID::HTTP_REQUEST_63": "route:GET /relative-redirect/{n}",
"GENERATE_ID::HTTP_REQUEST_64": "route:GET /response-headers",
"GENERATE_ID::HTTP_REQUEST_65": "route:POST /response-headers",
"GENERATE_ID::HTTP_REQUEST_66": "route:GET /robots.txt",
"GENERATE_ID::HTTP_REQUEST_67": "route:DELETE /status/{codes}",
"GENERATE_ID::HTTP_REQUEST_68": "route:GET /status/{codes}",
"GENERATE_ID::HTTP_REQUEST_69": "route:PATCH /status/{codes}",
"GENERATE_ID::HTTP_REQUEST_7": "route:DELETE /anything/{anything}",
"GENERATE_ID::HTTP_REQUEST_70": "route:POST /status/{codes}",
"GENERATE_ID::HTTP_REQUEST_71": "route:PUT /status/{codes}",
"GENERATE_ID::HTTP_REQUEST_72": "route:TRACE /status/{codes}",
"GENERATE_ID::HTTP_REQUEST_73": "route:GET /stream-bytes/{n}",
"GENERATE_ID::HTTP_REQUEST_74": "route:GET /stream/{n}",
"GENERATE_ID::HTTP_REQUEST_75": "route:GET /user-agent",
"GENERATE_ID::HTTP_REQUEST_76": "route:GET /uuid",
"GENERATE_ID::HTTP_REQUEST_77": "route:GET /xml",
"GENERATE_ID::HTTP_REQUEST_8": "route:GET /anything/{anything}",
"GENERATE_ID::HTTP_REQUEST_9": "route:PATCH /anything/{anything}",
},
}
`;
@@ -2734,6 +2835,10 @@ License: Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)",
},
],
},
"sourceKeys": {
"GENERATE_ID::FOLDER_0": "tag:request tag",
"GENERATE_ID::HTTP_REQUEST_0": "route:GET /apod",
},
}
`;
@@ -2834,5 +2939,9 @@ Responses:
},
],
},
"sourceKeys": {
"GENERATE_ID::HTTP_REQUEST_0": "route:GET /info.0.json",
"GENERATE_ID::HTTP_REQUEST_1": "route:GET /{comicId}/info.0.json",
},
}
`;
@@ -2382,4 +2382,38 @@ describe("importer-openapi", () => {
expect(imported).toMatchSnapshot();
});
}
test("Keys operations by operationId, unchanged by a rename", async () => {
const spec = (summary: string) =>
JSON.stringify({
openapi: "3.0.0",
info: { title: "Keys", version: "1" },
paths: {
"/pets": {
get: { operationId: "listPets", summary, tags: ["pets"], responses: {} },
},
},
});
const before = await convertOpenApi(spec("List pets"));
const after = await convertOpenApi(spec("Fetch every pet"));
expect(before?.sourceKeys?.[before.resources.httpRequests[0]!.id]).toBe("op:listPets");
expect(after?.sourceKeys?.[after.resources.httpRequests[0]!.id]).toBe("op:listPets");
expect(before?.sourceKeys?.[before.resources.folders[0]!.id]).toBe("tag:pets");
});
test("Falls back to the route when an operation has no operationId", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.0.0",
info: { title: "Keys", version: "1" },
paths: { "/pets/{id}": { delete: { summary: "Remove", responses: {} } } },
}),
);
expect(imported?.sourceKeys?.[imported.resources.httpRequests[0]!.id]).toBe(
"route:DELETE /pets/{id}",
);
});
});
+10 -1
View File
@@ -49,6 +49,12 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
const globalAuth = importAuth(root.auth);
const sourceKeys: Record<string, string> = {};
const trackSourceKey = (modelId: string, v: Record<string, unknown>, prefix: string) => {
const id = v.id ?? v._postman_id;
if (typeof id === "string" && id !== "") sourceKeys[modelId] = `${prefix}:${id}`;
};
const exportResources: ExportResources = {
workspaces: [],
environments: [],
@@ -63,6 +69,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
description: importDescription(info.description),
...globalAuth,
};
trackSourceKey(workspace.id, info, "collection");
exportResources.workspaces.push(workspace);
// Create the base environment
@@ -92,6 +99,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
name: v.name,
folderId,
};
trackSourceKey(folder.id, v, "item");
exportResources.folders.push(folder);
for (const child of v.item) {
importItem(child, folder.id);
@@ -142,6 +150,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
headers,
...requestAuth,
};
trackSourceKey(request.id, v, "item");
exportResources.httpRequests.push(request);
} else {
console.log("Unknown item", v, folderId);
@@ -156,7 +165,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources };
return { resources, sourceKeys };
}
function convertUrl(rawUrl: unknown): Pick<HttpRequest, "url" | "urlParameters"> {
@@ -300,5 +300,8 @@
}
],
"folders": []
},
"sourceKeys": {
"GENERATE_ID::WORKSPACE_0": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
}
}
@@ -88,5 +88,8 @@
"folderId": "GENERATE_ID::FOLDER_0"
}
]
},
"sourceKeys": {
"GENERATE_ID::WORKSPACE_1": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
}
}
@@ -100,5 +100,8 @@
}
],
"folders": []
},
"sourceKeys": {
"GENERATE_ID::WORKSPACE_2": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d"
}
}
@@ -87,4 +87,55 @@ describe("importer-postman", () => {
}),
]);
});
test("Keys items by their Postman ID, unchanged by a rename", () => {
const collection = (requestName: string) =>
JSON.stringify({
info: {
_postman_id: "collection-id",
name: "Keys",
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
},
item: [
{
id: "folder-id",
name: "Folder",
item: [
{
id: "request-id",
name: requestName,
request: { method: "GET", url: "https://yaak.app" },
},
],
},
],
});
const before = convertPostman(collection("Original"));
const after = convertPostman(collection("Renamed"));
const keyOf = (result: ReturnType<typeof convertPostman>, id: string | undefined) =>
id == null ? undefined : result?.sourceKeys?.[id];
expect(keyOf(before, before?.resources.httpRequests[0]?.id)).toBe("item:request-id");
expect(keyOf(after, after?.resources.httpRequests[0]?.id)).toBe("item:request-id");
expect(keyOf(before, before?.resources.folders[0]?.id)).toBe("item:folder-id");
expect(keyOf(before, before?.resources.workspaces[0]?.id)).toBe("collection:collection-id");
});
test("Omits keys for items the collection never identified", () => {
const result = convertPostman(
JSON.stringify({
info: {
name: "No IDs",
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
},
item: [{ name: "Request", request: { method: "GET", url: "https://yaak.app" } }],
}),
);
const requestId = result?.resources.httpRequests[0]?.id;
expect(requestId).toBeDefined();
expect(result?.sourceKeys).not.toHaveProperty(requestId as string);
});
});
+9 -1
View File
@@ -80,7 +80,15 @@ export function migrateImport(contents: string) {
}
}
return { resources: parsed.resources };
const sourceKeys: Record<string, string> = {};
for (const models of Object.values(parsed.resources)) {
if (!Array.isArray(models)) continue;
for (const model of models) {
if (typeof model?.id === "string") sourceKeys[model.id] = model.id;
}
}
return { resources: parsed.resources, sourceKeys };
}
function isJSObject(obj: unknown) {
+28
View File
@@ -148,4 +148,32 @@ describe("importer-yaak", () => {
}),
);
});
test("Keys models by their Yaak ID, unchanged by a rename", () => {
const exported = (requestName: string) =>
JSON.stringify({
yaakSchema: 5,
resources: {
workspaces: [{ id: "wk_1", model: "workspace", name: "Keys" }],
httpRequests: [
{
id: "rq_1",
model: "http_request",
workspaceId: "wk_1",
name: requestName,
url: "https://yaak.app",
},
],
},
});
expect(migrateImport(exported("Original"))?.sourceKeys).toEqual({
wk_1: "wk_1",
rq_1: "rq_1",
});
expect(migrateImport(exported("Renamed"))?.sourceKeys).toEqual({
wk_1: "wk_1",
rq_1: "rq_1",
});
});
});