diff --git a/package-lock.json b/package-lock.json index 6be73c28..b3b9a7c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index e6bc3ade..abcc0fa7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,6 @@ "workspaces-run": "^1.0.2" }, "dependencies": { - "@yaakapp/api": "^0.2.16" + "@yaakapp/api": "^0.2.17" } } diff --git a/plugins/importer-insomnia/src/index.ts b/plugins/importer-insomnia/src/index.ts index 9f320b59..12d53c47 100644 --- a/plugins/importer-insomnia/src/index.ts +++ b/plugins/importer-insomnia/src/index.ts @@ -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 = Partial & Pick; @@ -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) { - 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(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; + } +} diff --git a/plugins/importer-insomnia/tests/fixtures/basic.input.json b/plugins/importer-insomnia/tests/fixtures/basic.input.json new file mode 100644 index 00000000..cb0568e5 --- /dev/null +++ b/plugins/importer-insomnia/tests/fixtures/basic.input.json @@ -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" + } + ] +} diff --git a/plugins/importer-insomnia/tests/fixtures/basic.output.json b/plugins/importer-insomnia/tests/fixtures/basic.output.json new file mode 100644 index 00000000..f989ec0e --- /dev/null +++ b/plugins/importer-insomnia/tests/fixtures/basic.output.json @@ -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" + } + ] + } +} diff --git a/plugins/importer-insomnia/tests/index.test.ts b/plugins/importer-insomnia/tests/index.test.ts new file mode 100644 index 00000000..fe073717 --- /dev/null +++ b/plugins/importer-insomnia/tests/index.test.ts @@ -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)); + }); + } +}); diff --git a/plugins/importer-postman/src/index.ts b/plugins/importer-postman/src/index.ts index b4376aa9..93c25348 100644 --- a/plugins/importer-postman/src/index.ts +++ b/plugins/importer-postman/src/index.ts @@ -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, 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 { @@ -326,6 +338,20 @@ function convertTemplateSyntax(obj: T): T { } } +function deleteUndefinedAttrs(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> = {}; function generateId(model: string): string { diff --git a/plugins/importer-postman/tests/fixtures/nested.output.json b/plugins/importer-postman/tests/fixtures/nested.output.json index 9b7cc2c5..9e9bfaab 100644 --- a/plugins/importer-postman/tests/fixtures/nested.output.json +++ b/plugins/importer-postman/tests/fixtures/nested.output.json @@ -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", diff --git a/plugins/importer-postman/tests/fixtures/params.output.json b/plugins/importer-postman/tests/fixtures/params.output.json index 2ad692ae..1eb16ce3 100644 --- a/plugins/importer-postman/tests/fixtures/params.output.json +++ b/plugins/importer-postman/tests/fixtures/params.output.json @@ -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", diff --git a/plugins/importer-yaak/src/index.ts b/plugins/importer-yaak/src/index.ts index 8e53dd4d..136bc745 100644 --- a/plugins/importer-yaak/src/index.ts +++ b/plugins/importer-yaak/src/index.ts @@ -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 = { + 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 } diff --git a/plugins/importer-yaak/tests/index.test.ts b/plugins/importer-yaak/tests/index.test.ts index 0e4187d2..3c47c35a 100644 --- a/plugins/importer-yaak/tests/index.test.ts +++ b/plugins/importer-yaak/tests/index.test.ts @@ -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!' }], + }], + }, + }), + ); + }); });