Update for standalone base environments

This commit is contained in:
Gregory Schier
2025-01-13 17:04:35 -08:00
parent f8b211be1c
commit a80a25a90e
11 changed files with 495 additions and 51 deletions

8
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"plugins/*"
],
"dependencies": {
"@yaakapp/api": "^0.2.16"
"@yaakapp/api": "^0.2.17"
},
"devDependencies": {
"@types/node": "^22.7.4",
@@ -1003,9 +1003,9 @@
}
},
"node_modules/@yaakapp/api": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.2.16.tgz",
"integrity": "sha512-rooweCKOMsqbTdSlb4vxe3wL19PpkVualZrtWvRelnUhIPgcJR8EMVNn/K2tZfLGKOXnthZi9xgFBeARnOyuSw==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.2.17.tgz",
"integrity": "sha512-4ldxDxz2x4WCl4LR/D8Z6zyQGuMhBX3c4eMGDqxCjtEd5tXWaKJYQBEdi/Hp2FG0NSPNBEtyVfZd52sGfiqBoA==",
"dependencies": {
"@types/node": "^22.5.4"
}

View File

@@ -19,6 +19,6 @@
"workspaces-run": "^1.0.2"
},
"dependencies": {
"@yaakapp/api": "^0.2.16"
"@yaakapp/api": "^0.2.17"
}
}

View File

@@ -1,4 +1,4 @@
import { Environment, Folder, GrpcRequest, HttpRequest, Workspace, Context } from '@yaakapp/api';
import { Context, Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import YAML from 'yaml';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
@@ -16,11 +16,13 @@ export function pluginHookImport(ctx: Context, contents: string) {
try {
parsed = JSON.parse(contents);
} catch (e) {}
} catch (e) {
}
try {
parsed = parsed ?? YAML.parse(contents);
} catch (e) { }
} catch (e) {
}
if (!isJSObject(parsed)) return;
if (!Array.isArray(parsed.resources)) return;
@@ -35,23 +37,20 @@ export function pluginHookImport(ctx: Context, contents: string) {
// Import workspaces
const workspacesToImport = parsed.resources.filter(isWorkspace);
for (const workspaceToImport of workspacesToImport) {
const baseEnvironment = parsed.resources.find(
(r: any) => isEnvironment(r) && r.parentId === workspaceToImport._id,
);
for (const w of workspacesToImport) {
resources.workspaces.push({
id: convertId(workspaceToImport._id),
createdAt: new Date(workspacesToImport.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(workspacesToImport.updated ?? Date.now()).toISOString().replace('Z', ''),
id: convertId(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',
name: workspaceToImport.name,
variables: baseEnvironment ? parseVariables(baseEnvironment.data) : [],
name: w.name,
description: w.description || undefined,
});
const environmentsToImport = parsed.resources.filter(
(r: any) => isEnvironment(r) && r.parentId === baseEnvironment?._id,
(r: any) => isEnvironment(r),
);
resources.environments.push(
...environmentsToImport.map((r: any) => importEnvironment(r, workspaceToImport._id)),
...environmentsToImport.map((r: any) => importEnvironment(r, w._id)),
);
const nextFolder = (parentId: string) => {
@@ -59,22 +58,22 @@ export function pluginHookImport(ctx: Context, contents: string) {
let sortPriority = 0;
for (const child of children) {
if (isRequestGroup(child)) {
resources.folders.push(importFolder(child, workspaceToImport._id));
resources.folders.push(importFolder(child, w._id));
nextFolder(child._id);
} else if (isHttpRequest(child)) {
resources.httpRequests.push(
importHttpRequest(child, workspaceToImport._id, sortPriority++),
importHttpRequest(child, w._id, sortPriority++),
);
} else if (isGrpcRequest(child)) {
resources.grpcRequests.push(
importGrpcRequest(child, workspaceToImport._id, sortPriority++),
importGrpcRequest(child, w._id, sortPriority++),
);
}
}
};
// Import folders
nextFolder(workspaceToImport._id);
nextFolder(w._id);
}
// Filter out any `null` values
@@ -83,15 +82,16 @@ export function pluginHookImport(ctx: Context, contents: string) {
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
return { resources: deleteUndefinedAttrs(resources) };
}
function importEnvironment(e: any, workspaceId: string): ExportResources['environments'][0] {
return {
id: convertId(e._id),
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
updatedAt: e.updated ? new Date(e.updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
environmentId: e.parentId === workspaceId ? null : convertId(e.parentId),
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
@@ -105,10 +105,11 @@ function importEnvironment(e: any, workspaceId: string): ExportResources['enviro
function importFolder(f: any, workspaceId: string): ExportResources['folders'][0] {
return {
id: convertId(f._id),
createdAt: new Date(f.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace('Z', ''),
createdAt: f.created ? new Date(f.created).toISOString().replace('Z', '') : undefined,
updatedAt: f.updated ? new Date(f.updated).toISOString().replace('Z', '') : undefined,
folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
workspaceId: convertId(workspaceId),
description: f.description || undefined,
model: 'folder',
name: f.name,
};
@@ -125,13 +126,14 @@ function importGrpcRequest(
return {
id: convertId(r._id),
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
updatedAt: r.updated ? new Date(r.updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'grpc_request',
sortPriority,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
service,
method,
@@ -200,13 +202,14 @@ function importHttpRequest(
return {
id: convertId(r._id),
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
createdAt: r.created ? new Date(r.created).toISOString().replace('Z', '') : undefined,
updatedAt: r.updated ? new Date(r.updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
folderId: r.parentId === workspaceId ? null : convertId(r.parentId),
model: 'http_request',
sortPriority,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
body,
bodyType,
@@ -223,14 +226,6 @@ function importHttpRequest(
};
}
function parseVariables(data: Record<string, string>) {
return Object.entries(data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
}));
}
function convertSyntax(variable: string): string {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
@@ -270,3 +265,17 @@ function convertId(id: string): string {
}
return `GENERATE_ID::${id}`;
}
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)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
}

View File

@@ -0,0 +1,187 @@
{
"_type": "export",
"__export_format": 4,
"__export_date": "2025-01-13T15:19:18.330Z",
"__export_source": "insomnia.desktop.app:v10.3.0",
"resources": [
{
"_id": "req_84cd9ae4bd034dd8bb730e856a665cbb",
"parentId": "fld_859d1df78261463480b6a3a1419517e3",
"modified": 1736781473176,
"created": 1736781406672,
"url": "{{ _.BASE_URL }}/foo/:id",
"name": "New Request",
"description": "My description of the request",
"method": "GET",
"body": {
"mimeType": "multipart/form-data",
"params": [
{
"id": "pair_7c86036ae8ef499dbbc0b43d0800c5a3",
"name": "form",
"value": "data",
"description": "",
"disabled": false
}
]
},
"parameters": [
{
"id": "pair_b22f6ff611cd4250a6e405ca7b713d09",
"name": "query",
"value": "qqq",
"description": "",
"disabled": false
}
],
"headers": [
{
"name": "Content-Type",
"value": "multipart/form-data",
"id": "pair_4af845963bd14256b98716617971eecd"
},
{
"name": "User-Agent",
"value": "insomnia/10.3.0",
"id": "pair_535ffd00ce48462cb1b7258832ade65a"
},
{
"id": "pair_ab4b870278e943cba6babf5a73e213e3",
"name": "X-Header",
"value": "xxxx",
"description": "",
"disabled": false
}
],
"authentication": {
"type": "basic",
"useISO88591": false,
"disabled": false,
"username": "user",
"password": "pass"
},
"metaSortKey": -1736781406672,
"isPrivate": false,
"pathParameters": [
{
"name": "id",
"value": "iii"
}
],
"settingStoreCookies": true,
"settingSendCookies": true,
"settingDisableRenderRequestBody": false,
"settingEncodeUrl": true,
"settingRebuildPath": true,
"settingFollowRedirects": "global",
"_type": "request"
},
{
"_id": "fld_859d1df78261463480b6a3a1419517e3",
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
"modified": 1736781404718,
"created": 1736781404718,
"name": "Top Level",
"description": "",
"environment": {},
"environmentPropertyOrder": null,
"metaSortKey": -1736781404718,
"environmentType": "kv",
"_type": "request_group"
},
{
"_id": "wrk_d4d92f7c0ee947b89159243506687019",
"parentId": null,
"modified": 1736781343765,
"created": 1736781343765,
"name": "Dummy",
"description": "",
"scope": "collection",
"_type": "workspace"
},
{
"_id": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
"modified": 1736781355209,
"created": 1736781343767,
"name": "Base Environment",
"data": {
"BASE_VAR": "hello"
},
"dataPropertyOrder": null,
"color": null,
"isPrivate": false,
"metaSortKey": 1736781343767,
"environmentType": "kv",
"kvPairData": [
{
"id": "envPair_61c1be66d42241b5a28306d2cd92d3e3",
"name": "BASE_VAR",
"value": "hello",
"type": "str",
"enabled": true
}
],
"_type": "environment"
},
{
"_id": "jar_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"parentId": "wrk_d4d92f7c0ee947b89159243506687019",
"modified": 1736781343768,
"created": 1736781343768,
"name": "Default Jar",
"cookies": [],
"_type": "cookie_jar"
},
{
"_id": "env_799ae3d723ef44af91b4817e5d057e6d",
"parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"modified": 1736781394705,
"created": 1736781358515,
"name": "Production",
"data": {
"BASE_URL": "https://api.yaak.app"
},
"dataPropertyOrder": null,
"color": "#f22c2c",
"isPrivate": false,
"metaSortKey": 1736781358515,
"environmentType": "kv",
"kvPairData": [
{
"id": "envPair_4d97b569b7e845ccbf488e1b26637cbc",
"name": "BASE_URL",
"value": "https://api.yaak.app",
"type": "str",
"enabled": true
}
],
"_type": "environment"
},
{
"_id": "env_030fbfdbb274426ebd78e2e6518f8553",
"parentId": "env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"modified": 1736781391078,
"created": 1736781374707,
"name": "Staging",
"data": {
"BASE_URL": "https://api.staging.yaak.app"
},
"dataPropertyOrder": null,
"color": "#206fac",
"isPrivate": false,
"metaSortKey": 1736781358565,
"environmentType": "kv",
"kvPairData": [
{
"id": "envPair_4d97b569b7e845ccbf488e1b26637cbc",
"name": "BASE_URL",
"value": "https://api.staging.yaak.app",
"type": "str",
"enabled": true
}
],
"_type": "environment"
}
]
}

View File

@@ -0,0 +1,117 @@
{
"resources": {
"environments": [
{
"createdAt": "2025-01-13T15:15:43.767",
"environmentId": null,
"id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"model": "environment",
"name": "Base Environment",
"variables": [
{
"enabled": true,
"name": "BASE_VAR",
"value": "hello"
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
},
{
"createdAt": "2025-01-13T15:15:58.515",
"environmentId": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d",
"model": "environment",
"name": "Production",
"variables": [
{
"enabled": true,
"name": "BASE_URL",
"value": "https://api.yaak.app"
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
},
{
"createdAt": "2025-01-13T15:16:14.707",
"environmentId": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553",
"model": "environment",
"name": "Staging",
"variables": [
{
"enabled": true,
"name": "BASE_URL",
"value": "https://api.staging.yaak.app"
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
}
],
"folders": [
{
"createdAt": "2025-01-13T15:16:44.718",
"folderId": null,
"id": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3",
"model": "folder",
"name": "Top Level",
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
}
],
"grpcRequests": [],
"httpRequests": [
{
"authentication": {
"password": "pass",
"username": "user"
},
"authenticationType": "basic",
"body": {
"form": [
{
"enabled": true,
"file": null,
"name": "form",
"value": "data"
}
]
},
"bodyType": "multipart/form-data",
"createdAt": "2025-01-13T15:16:46.672",
"description": "My description of the request",
"folderId": "GENERATE_ID::fld_859d1df78261463480b6a3a1419517e3",
"headers": [
{
"enabled": true,
"name": "Content-Type",
"value": "multipart/form-data"
},
{
"enabled": true,
"name": "User-Agent",
"value": "insomnia/10.3.0"
},
{
"enabled": true,
"name": "X-Header",
"value": "xxxx"
}
],
"id": "GENERATE_ID::req_84cd9ae4bd034dd8bb730e856a665cbb",
"method": "GET",
"model": "http_request",
"name": "New Request",
"sortPriority": 0,
"url": "${[BASE_URL ]}/foo/:id",
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
}
],
"workspaces": [
{
"createdAt": "2025-01-13T15:15:43.765",
"id": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019",
"model": "workspace",
"name": "Dummy"
}
]
}
}

View File

@@ -0,0 +1,26 @@
import { Context } from '@yaakapp/api';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import { pluginHookImport } from '../src';
const ctx = {} as Context;
describe('importer-yaak', () => {
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 = pluginHookImport(ctx, contents);
// console.log(JSON.stringify(result, null, 2))
expect(result).toEqual(JSON.parse(expected));
});
}
});

View File

@@ -47,14 +47,23 @@ export function pluginHookImport(
model: 'workspace',
id: generateId('workspace'),
name: info.name || 'Postman Import',
description: info.description?.content ?? info.description ?? '',
description: info.description?.content ?? info.description,
};
exportResources.workspaces.push(workspace);
// Create the base environment
const environment: ExportResources['environments'][0] = {
model: 'environment',
id: generateId('environment'),
name: 'Global Variables',
workspaceId: workspace.id,
variables:
root.variable?.map((v: any) => ({
name: v.key,
value: v.value,
})) ?? [],
};
exportResources.workspaces.push(workspace);
exportResources.environments.push(environment);
const importItem = (v: Record<string, any>, folderId: string | null = null) => {
if (typeof v.name === 'string' && Array.isArray(v.item)) {
@@ -100,6 +109,7 @@ export function pluginHookImport(
workspaceId: workspace.id,
folderId,
name: v.name,
description: v.description || undefined,
method: r.method || 'GET',
url,
urlParameters,
@@ -119,7 +129,9 @@ export function pluginHookImport(
importItem(item);
}
return { resources: convertTemplateSyntax(exportResources) };
const resources = deleteUndefinedAttrs(convertTemplateSyntax(exportResources));
return { resources };
}
function convertUrl(url: string | any): Pick<HttpRequest, 'url' | 'urlParameters'> {
@@ -326,6 +338,20 @@ function convertTemplateSyntax<T>(obj: T): T {
}
}
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)
.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 {

View File

@@ -4,12 +4,18 @@
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_0",
"name": "New Collection",
"description": "",
"variables": []
"name": "New Collection"
}
],
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_0",
"model": "environment",
"name": "Global Variables",
"variables": [],
"workspaceId": "GENERATE_ID::WORKSPACE_0"
}
],
"environments": [],
"httpRequests": [
{
"model": "http_request",

View File

@@ -4,8 +4,15 @@
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_1",
"name": "New Collection",
"description": "",
"name": "New Collection"
}
],
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_1",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"model": "environment",
"name": "Global Variables",
"variables": [
{
"name": "COLLECTION VARIABLE",
@@ -14,7 +21,6 @@
]
}
],
"environments": [],
"httpRequests": [
{
"model": "http_request",

View File

@@ -1,4 +1,4 @@
import { Context } from '@yaakapp/api';
import { Context, Environment } from '@yaakapp/api';
export function pluginHookImport(_ctx: Context, contents: string) {
let parsed;
@@ -23,6 +23,31 @@ export function pluginHookImport(_ctx: Context, contents: string) {
delete parsed.resources['requests'];
}
// Migrate v2 to v3
for (const workspace of parsed.resources.workspaces ?? []) {
if ('variables' in workspace) {
// Create the base environment
const baseEnvironment: Partial<Environment> = {
id: `GENERATE_ID::base_env_${workspace['id']}`,
name: 'Global Variables',
variables: workspace.variables,
workspaceId: workspace.id,
};
parsed.resources.environments = parsed.resources.environments ?? [];
parsed.resources.environments.push(baseEnvironment);
// Delete variables key from workspace
delete workspace.variables;
// Add environmentId to relevant environments
for (const environment of parsed.resources.environments) {
if (environment.workspaceId === workspace.id && environment.id !== baseEnvironment.id) {
environment.environmentId = baseEnvironment.id;
}
}
}
}
return { resources: parsed.resources }; // Should already be in the correct format
}

View File

@@ -30,4 +30,46 @@ describe('importer-yaak', () => {
}),
);
});
test('converts schema 2 to 3', () => {
const imported = pluginHookImport(
ctx,
JSON.stringify({
yaakSchema: 2,
resources: {
environments: [{
id: 'e_1',
workspaceId: 'w_1',
name: 'Production',
variables: [{ name: 'E1', value: 'E1!' }],
}],
workspaces: [{
id: 'w_1',
variables: [{ name: 'W1', value: 'W1!' }],
}],
},
}),
);
expect(imported).toEqual(
expect.objectContaining({
resources: {
workspaces: [{
id: 'w_1',
}],
environments: [{
id: 'e_1',
environmentId: 'GENERATE_ID::base_env_w_1',
workspaceId: 'w_1',
name: 'Production',
variables: [{ name: 'E1', value: 'E1!' }],
}, {
id: 'GENERATE_ID::base_env_w_1',
workspaceId: 'w_1',
name: 'Global Variables',
variables: [{ name: 'W1', value: 'W1!' }],
}],
},
}),
);
});
});