diff --git a/packages/insomnia-app/app/__jest__/redux-state-for-test.ts b/packages/insomnia-app/app/__jest__/redux-state-for-test.ts index fdd83e1925..93232cbf91 100644 --- a/packages/insomnia-app/app/__jest__/redux-state-for-test.ts +++ b/packages/insomnia-app/app/__jest__/redux-state-for-test.ts @@ -1,4 +1,5 @@ import { ACTIVITY_DEBUG } from '../common/constants'; +import { BASE_SPACE_ID } from '../models/space'; import { RootState } from '../ui/redux/modules'; import * as entities from '../ui/redux/modules/entities'; import { GlobalState } from '../ui/redux/modules/global'; @@ -8,7 +9,7 @@ export const reduxStateForTest = async (global: Partial = {}): Prom global: { activeWorkspaceId: null, activeActivity: ACTIVITY_DEBUG, - activeSpaceId: null, + activeSpaceId: BASE_SPACE_ID, isLoading: false, isLoggedIn: false, loadingRequestIds: {}, diff --git a/packages/insomnia-app/app/common/__tests__/database.test.ts b/packages/insomnia-app/app/common/__tests__/database.test.ts index a28f8688f9..1373fd41dd 100644 --- a/packages/insomnia-app/app/common/__tests__/database.test.ts +++ b/packages/insomnia-app/app/common/__tests__/database.test.ts @@ -263,8 +263,10 @@ describe('_repairDatabase()', () => { it('fixes duplicate environments', async () => { // Create Workspace with no children + const space = await models.space.create(); const workspace = await models.workspace.create({ _id: 'w1', + parentId: space._id, }); const spec = await models.apiSpec.getByParentId(workspace._id); expect((await db.withDescendants(workspace)).length).toBe(2); @@ -326,7 +328,7 @@ describe('_repairDatabase()', () => { { _id: 'w1', data: null, - parentId: null, + parentId: workspace.parentId, }, { _id: 'b1', @@ -392,7 +394,7 @@ describe('_repairDatabase()', () => { { _id: 'w1', data: null, - parentId: null, + parentId: workspace.parentId, }, { _id: 'b1', @@ -442,8 +444,10 @@ describe('_repairDatabase()', () => { it('fixes duplicate cookie jars', async () => { // Create Workspace with no children + const space = await models.space.create(); const workspace = await models.workspace.create({ _id: 'w1', + parentId: space._id, }); const spec = await models.apiSpec.getByParentId(workspace._id); expect((await db.withDescendants(workspace)).length).toBe(2); @@ -496,7 +500,7 @@ describe('_repairDatabase()', () => { { _id: 'w1', cookies: null, - parentId: null, + parentId: workspace.parentId, }, { _id: 'j1', @@ -548,7 +552,7 @@ describe('_repairDatabase()', () => { { _id: 'w1', cookies: null, - parentId: null, + parentId: workspace.parentId, }, { _id: 'j1', @@ -713,7 +717,10 @@ describe('withAncestors()', () => { beforeEach(globalBeforeEach); it('should return itself and all parents but exclude siblings', async () => { - const wrk = await models.workspace.create(); + const spc = await models.space.create(); + const wrk = await models.workspace.create({ + parentId: spc._id, + }); const wrkReq = await models.request.create({ parentId: wrk._id, }); @@ -730,14 +737,14 @@ describe('withAncestors()', () => { parentId: grp._id, }); // Workspace child searching for ancestors - await expect(db.withAncestors(wrk)).resolves.toStrictEqual([wrk]); - await expect(db.withAncestors(wrkReq)).resolves.toStrictEqual([wrkReq, wrk]); - await expect(db.withAncestors(wrkGrpcReq)).resolves.toStrictEqual([wrkGrpcReq, wrk]); + await expect(db.withAncestors(wrk)).resolves.toStrictEqual([wrk, spc]); + await expect(db.withAncestors(wrkReq)).resolves.toStrictEqual([wrkReq, wrk, spc]); + await expect(db.withAncestors(wrkGrpcReq)).resolves.toStrictEqual([wrkGrpcReq, wrk, spc]); // Group searching for ancestors - await expect(db.withAncestors(grp)).resolves.toStrictEqual([grp, wrk]); + await expect(db.withAncestors(grp)).resolves.toStrictEqual([grp, wrk, spc]); // Group child searching for ancestors - await expect(db.withAncestors(grpReq)).resolves.toStrictEqual([grpReq, grp, wrk]); - await expect(db.withAncestors(grpGrpcReq)).resolves.toStrictEqual([grpGrpcReq, grp, wrk]); + await expect(db.withAncestors(grpReq)).resolves.toStrictEqual([grpReq, grp, wrk, spc]); + await expect(db.withAncestors(grpGrpcReq)).resolves.toStrictEqual([grpGrpcReq, grp, wrk, spc]); // Group child searching for ancestors with filters await expect(db.withAncestors(grpGrpcReq, [models.requestGroup.type])).resolves.toStrictEqual([ grpGrpcReq, diff --git a/packages/insomnia-app/app/common/__tests__/export.test.ts b/packages/insomnia-app/app/common/__tests__/export.test.ts index aea519efad..f555648191 100644 --- a/packages/insomnia-app/app/common/__tests__/export.test.ts +++ b/packages/insomnia-app/app/common/__tests__/export.test.ts @@ -6,7 +6,10 @@ import { getAppVersion } from '../constants'; import { exportRequestsData, exportRequestsHAR, exportWorkspacesData, exportWorkspacesHAR } from '../export'; describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { - beforeEach(globalBeforeEach); + beforeEach(async () => { + await globalBeforeEach(); + await models.space.all(); + }); it('exports a single workspace and some requests only as an HTTP Archive', async () => { const wrk1 = await models.workspace.create({ @@ -172,7 +175,7 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { activeEnvironmentId: env2Private._id, }); const includePrivateDocs = false; - const json = await exportWorkspacesHAR([], includePrivateDocs); + const json = await exportWorkspacesHAR([wrk1, wrk2], includePrivateDocs); const data = JSON.parse(json); expect(data).toMatchObject({ log: { @@ -260,8 +263,8 @@ describe('export', () => { parentId: eBase._id, }); // Test export whole workspace. - const exportedWorkspacesJson = await exportWorkspacesData([], false, 'json'); - const exportedWorkspacesYaml = await exportWorkspacesData([], false, 'yaml'); + const exportedWorkspacesJson = await exportWorkspacesData([w], false, 'json'); + const exportedWorkspacesYaml = await exportWorkspacesData([w], false, 'yaml'); const exportWorkspacesDataJson = JSON.parse(exportedWorkspacesJson); const exportWorkspacesDataYaml = YAML.parse(exportedWorkspacesYaml); // Ensure JSON is the same as YAML @@ -276,7 +279,7 @@ describe('export', () => { _id: w._id, }), expect.objectContaining({ - _id: spec._id, + _id: spec?._id, }), expect.objectContaining({ _id: eBase._id, diff --git a/packages/insomnia-app/app/common/__tests__/har.test.ts b/packages/insomnia-app/app/common/__tests__/har.test.ts index b939e1e67a..f7cc428d88 100644 --- a/packages/insomnia-app/app/common/__tests__/har.test.ts +++ b/packages/insomnia-app/app/common/__tests__/har.test.ts @@ -6,543 +6,544 @@ import { AUTH_BASIC } from '../constants'; import * as harUtils from '../har'; import * as render from '../render'; -describe('exportHar()', () => { - beforeEach(globalBeforeEach); +describe('export', () => { + beforeEach(async () => { + await globalBeforeEach(); + await models.space.all(); + }); - it('exports single requests', async () => { - const wrk = await models.workspace.create({ - _id: 'wrk_1', - name: 'Workspace', - }); - const req1 = await models.request.create({ - _id: 'req_1', - name: 'Request 1', - parentId: wrk._id, - url: 'https://httpstat.us/200', - method: 'POST', - body: { - mimeType: 'application/json', - text: '{}', - }, - headers: [ - { - name: 'Content-Type', - value: 'application/json', + describe('exportHar()', () => { + it('exports single requests', async () => { + const wrk = await models.workspace.create({ + _id: 'wrk_1', + name: 'Workspace', + }); + const req1 = await models.request.create({ + _id: 'req_1', + name: 'Request 1', + parentId: wrk._id, + url: 'https://httpstat.us/200', + method: 'POST', + body: { + mimeType: 'application/json', + text: '{}', }, - { - name: 'Accept', - value: 'application/json', - disabled: false, - }, - { - name: 'X-Disabled', - value: 'X-Disabled', - disabled: true, - }, - ], - }); - await models.response.create({ - parentId: req1._id, - statusCode: 200, - statusMessage: 'OK', - elapsedTime: 999, - headers: [ - { - name: 'Content-Type', - value: 'application/json', - }, - ], - contentType: 'application/json', - bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), - bodyCompression: null, - }); - const exportRequests = [ - { - requestId: req1._id, - environmentId: 'n/a', - }, - ]; - const harExport = await harUtils.exportHar(exportRequests); - expect(harExport).toMatchObject({ - log: { - version: '1.2', - creator: { - name: 'Insomnia REST Client', - }, - entries: [ + headers: [ { - startedDateTime: expect.any(String), - time: 999, - request: { - method: 'POST', - url: 'https://httpstat.us/200', - httpVersion: 'HTTP/1.1', - cookies: [], - headers: [ - { - name: 'Content-Type', - value: 'application/json', - }, - { - name: 'Accept', - value: 'application/json', - }, - ], - queryString: [], - postData: { - mimeType: 'application/json', - params: [], - text: '{}', - }, - headersSize: -1, - bodySize: -1, - }, - response: { - status: 200, - statusText: 'OK', - httpVersion: 'HTTP/1.1', - cookies: [], - headers: [ - { - name: 'Content-Type', - value: 'application/json', - }, - ], - content: { - size: 15, - mimeType: 'application/json', - text: '{"key":"value"}', - }, - redirectURL: '', - headersSize: -1, - bodySize: -1, - }, - cache: {}, - timings: { - blocked: -1, - dns: -1, - connect: -1, - send: 0, - wait: 999, - receive: 0, - ssl: -1, - }, - comment: req1.name, + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Accept', + value: 'application/json', + disabled: false, + }, + { + name: 'X-Disabled', + value: 'X-Disabled', + disabled: true, }, ], - }, - }); - }); - - it('exports multiple requests', async () => { - const workspace = await models.workspace.create({ - _id: 'wrk_1', - name: 'Workspace', - }); - const baseReq = await models.request.create({ - _id: 'req_0', - type: models.request.type, - name: 'Request', - parentId: workspace._id, - url: 'http://localhost', - method: 'GET', - body: {}, - headers: [ - { - name: 'X-Environment', - value: '{{ envvalue }}', - }, - ], - }); - const req1 = await models.request.duplicate(baseReq); - req1._id = 'req_1'; - req1.name = 'Request 1'; - req1.headers.push({ - name: 'X-Request', - value: '1', - }); - await models.request.create(req1); - const req2 = await models.request.duplicate(baseReq); - req2._id = 'req_2'; - req2.name = 'Request 2'; - req2.headers.push({ - name: 'X-Request', - value: '2', - }); - await models.request.create(req2); - const req3 = await models.request.duplicate(baseReq); - req3._id = 'req_3'; - req3.name = 'Request 3'; - req3.headers.push({ - name: 'X-Request', - value: '3', - }); - await models.request.create(req3); - const envBase = await models.environment.getOrCreateForParentId(workspace._id); - await models.environment.update(envBase, { - data: { - envvalue: '', - }, - }); - const envPublic = await models.environment.create({ - _id: 'env_1', - name: 'Public', - parentId: envBase._id, - data: { - envvalue: 'public', - }, - }); - const envPrivate = await models.environment.create({ - _id: 'env_2', - name: 'Private', - isPrivate: true, - parentId: envBase._id, - data: { - envvalue: 'private', - }, - }); - await models.response.create({ - _id: 'res_1', - parentId: req1._id, - statusCode: 204, - }); - await models.response.create({ - _id: 'res_2', - parentId: req2._id, - statusCode: 404, - }); - await models.response.create({ - _id: 'res_3', - parentId: req3._id, - statusCode: 500, - }); - const exportRequests = [ - { - requestId: req1._id, - environmentId: null, - }, - { - requestId: req2._id, - environmentId: envPublic._id, - }, - { - requestId: req3._id, - environmentId: envPrivate._id, - }, - ]; - const harExport = await harUtils.exportHar(exportRequests); - expect(harExport).toMatchObject({ - log: { - version: '1.2', - creator: { - name: 'Insomnia REST Client', - }, - entries: [ + }); + await models.response.create({ + parentId: req1._id, + statusCode: 200, + statusMessage: 'OK', + elapsedTime: 999, + headers: [ { - request: { - headers: [ - { - name: 'X-Environment', - value: '', - }, - { - name: 'X-Request', - value: '1', - }, - ], - }, - response: { - status: 204, - }, - comment: req1.name, - }, - { - request: { - headers: [ - { - name: 'X-Environment', - value: 'public', - }, - { - name: 'X-Request', - value: '2', - }, - ], - }, - response: { - status: 404, - }, - comment: req2.name, - }, - { - request: { - headers: [ - { - name: 'X-Environment', - value: 'private', - }, - { - name: 'X-Request', - value: '3', - }, - ], - }, - response: { - status: 500, - }, - comment: req3.name, + name: 'Content-Type', + value: 'application/json', }, ], - }, + contentType: 'application/json', + bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), + bodyCompression: null, + }); + const exportRequests = [ + { + requestId: req1._id, + environmentId: 'n/a', + }, + ]; + const harExport = await harUtils.exportHar(exportRequests); + expect(harExport).toMatchObject({ + log: { + version: '1.2', + creator: { + name: 'Insomnia REST Client', + }, + entries: [ + { + startedDateTime: expect.any(String), + time: 999, + request: { + method: 'POST', + url: 'https://httpstat.us/200', + httpVersion: 'HTTP/1.1', + cookies: [], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Accept', + value: 'application/json', + }, + ], + queryString: [], + postData: { + mimeType: 'application/json', + params: [], + text: '{}', + }, + headersSize: -1, + bodySize: -1, + }, + response: { + status: 200, + statusText: 'OK', + httpVersion: 'HTTP/1.1', + cookies: [], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], + content: { + size: 15, + mimeType: 'application/json', + text: '{"key":"value"}', + }, + redirectURL: '', + headersSize: -1, + bodySize: -1, + }, + cache: {}, + timings: { + blocked: -1, + dns: -1, + connect: -1, + send: 0, + wait: 999, + receive: 0, + ssl: -1, + }, + comment: req1.name, + }, + ], + }, + }); }); - }); -}); -describe('exportHarResponse()', () => { - beforeEach(globalBeforeEach); - - it('exports a default har response for an empty response', async () => { - const notFoundResponse = null; - const harResponse = await harUtils.exportHarResponse(notFoundResponse); - expect(harResponse).toMatchObject({ - status: 0, - statusText: '', - httpVersion: 'HTTP/1.1', - content: { - size: 0, - mimeType: '', - }, + it('exports multiple requests', async () => { + const workspace = await models.workspace.create({ + _id: 'wrk_1', + name: 'Workspace', + }); + const baseReq = await models.request.create({ + _id: 'req_0', + type: models.request.type, + name: 'Request', + parentId: workspace._id, + url: 'http://localhost', + method: 'GET', + body: {}, + headers: [ + { + name: 'X-Environment', + value: '{{ envvalue }}', + }, + ], + }); + const req1 = await models.request.duplicate(baseReq); + req1._id = 'req_1'; + req1.name = 'Request 1'; + req1.headers.push({ + name: 'X-Request', + value: '1', + }); + await models.request.create(req1); + const req2 = await models.request.duplicate(baseReq); + req2._id = 'req_2'; + req2.name = 'Request 2'; + req2.headers.push({ + name: 'X-Request', + value: '2', + }); + await models.request.create(req2); + const req3 = await models.request.duplicate(baseReq); + req3._id = 'req_3'; + req3.name = 'Request 3'; + req3.headers.push({ + name: 'X-Request', + value: '3', + }); + await models.request.create(req3); + const envBase = await models.environment.getOrCreateForParentId(workspace._id); + await models.environment.update(envBase, { + data: { + envvalue: '', + }, + }); + const envPublic = await models.environment.create({ + _id: 'env_1', + name: 'Public', + parentId: envBase._id, + data: { + envvalue: 'public', + }, + }); + const envPrivate = await models.environment.create({ + _id: 'env_2', + name: 'Private', + isPrivate: true, + parentId: envBase._id, + data: { + envvalue: 'private', + }, + }); + await models.response.create({ + _id: 'res_1', + parentId: req1._id, + statusCode: 204, + }); + await models.response.create({ + _id: 'res_2', + parentId: req2._id, + statusCode: 404, + }); + await models.response.create({ + _id: 'res_3', + parentId: req3._id, + statusCode: 500, + }); + const exportRequests = [ + { + requestId: req1._id, + environmentId: null, + }, + { + requestId: req2._id, + environmentId: envPublic._id, + }, + { + requestId: req3._id, + environmentId: envPrivate._id, + }, + ]; + const harExport = await harUtils.exportHar(exportRequests); + expect(harExport).toMatchObject({ + log: { + version: '1.2', + creator: { + name: 'Insomnia REST Client', + }, + entries: [ + { + request: { + headers: [ + { + name: 'X-Environment', + value: '', + }, + { + name: 'X-Request', + value: '1', + }, + ], + }, + response: { + status: 204, + }, + comment: req1.name, + }, + { + request: { + headers: [ + { + name: 'X-Environment', + value: 'public', + }, + { + name: 'X-Request', + value: '2', + }, + ], + }, + response: { + status: 404, + }, + comment: req2.name, + }, + { + request: { + headers: [ + { + name: 'X-Environment', + value: 'private', + }, + { + name: 'X-Request', + value: '3', + }, + ], + }, + response: { + status: 500, + }, + comment: req3.name, + }, + ], + }, + }); }); }); - it('exports a valid har response for a non empty response', async () => { - const response = Object.assign(models.response.init(), { - _id: 'res_123', - type: models.response.type, - parentId: 'req_123', - modified: 0, - created: 0, - statusCode: 200, - statusMessage: 'OK', - headers: [ - { - name: 'Content-Type', - value: 'application/json', + describe('exportHarResponse()', () => { + it('exports a default har response for an empty response', async () => { + const notFoundResponse = null; + const harResponse = await harUtils.exportHarResponse(notFoundResponse); + expect(harResponse).toMatchObject({ + status: 0, + statusText: '', + httpVersion: 'HTTP/1.1', + content: { + size: 0, + mimeType: '', }, - { - name: 'Content-Length', - value: '2', - }, - { - name: 'Set-Cookie', - value: 'sessionid=12345; HttpOnly; Path=/', - }, - ], - contentType: 'application/json', - bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), + }); }); - const harResponse = await harUtils.exportHarResponse(response); - expect(harResponse).toMatchObject({ - status: 200, - statusText: 'OK', - httpVersion: 'HTTP/1.1', - cookies: [ - { - name: 'sessionid', - value: '12345', - path: '/', - httpOnly: true, + + it('exports a valid har response for a non empty response', async () => { + const response = Object.assign(models.response.init(), { + _id: 'res_123', + type: models.response.type, + parentId: 'req_123', + modified: 0, + created: 0, + statusCode: 200, + statusMessage: 'OK', + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Content-Length', + value: '2', + }, + { + name: 'Set-Cookie', + value: 'sessionid=12345; HttpOnly; Path=/', + }, + ], + contentType: 'application/json', + bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), + }); + const harResponse = await harUtils.exportHarResponse(response); + expect(harResponse).toMatchObject({ + status: 200, + statusText: 'OK', + httpVersion: 'HTTP/1.1', + cookies: [ + { + name: 'sessionid', + value: '12345', + path: '/', + httpOnly: true, + }, + ], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Content-Length', + value: '2', + }, + { + name: 'Set-Cookie', + value: 'sessionid=12345; HttpOnly; Path=/', + }, + ], + content: { + size: 15, + mimeType: 'application/json', + text: '{"key":"value"}', }, - ], - headers: [ - { - name: 'Content-Type', - value: 'application/json', - }, - { - name: 'Content-Length', - value: '2', - }, - { - name: 'Set-Cookie', - value: 'sessionid=12345; HttpOnly; Path=/', - }, - ], - content: { - size: 15, - mimeType: 'application/json', - text: '{"key":"value"}', - }, - redirectURL: '', - headersSize: -1, - bodySize: -1, + redirectURL: '', + headersSize: -1, + bodySize: -1, + }); }); }); -}); -describe('exportHarWithRequest()', () => { - beforeEach(globalBeforeEach); - - it('renders does it correctly', async () => { - const workspace = await models.workspace.create(); - const cookies = [ - { - creation: new Date('2016-10-05T04:40:49.505Z'), - key: 'foo', - value: 'barrrrr', - expires: new Date('2096-10-12T04:40:49.000Z'), - domain: 'google.com', - path: '/', - hostOnly: true, - lastAccessed: new Date('2096-10-05T04:40:49.505Z'), - }, - ]; - const cookieJar = await models.cookieJar.getOrCreateForParentId(workspace._id); - await models.cookieJar.update(cookieJar, { - parentId: workspace._id, - cookies, - }); - const request = Object.assign(models.request.init(), { - _id: 'req_123', - parentId: workspace._id, - headers: [ + describe('exportHarWithRequest()', () => { + it('renders does it correctly', async () => { + const workspace = await models.workspace.create(); + const cookies = [ { - name: 'Content-Type', - value: 'application/json', - }, - ], - parameters: [ - { - name: 'foo bar', - value: 'hello&world', - }, - ], - method: 'POST', - body: { - text: 'foo bar', - }, - url: 'http://google.com', - authentication: { - type: AUTH_BASIC, - username: 'user', - password: 'pass', - }, - }); - const { request: renderedRequest } = await render.getRenderedRequestAndContext(request); - const har = await harUtils.exportHarWithRequest(renderedRequest); - expect(har.cookies.length).toBe(1); - expect(har).toEqual({ - bodySize: -1, - cookies: [ - { - domain: 'google.com', - expires: '2096-10-12T04:40:49.000Z', - name: 'foo', - path: '/', + creation: new Date('2016-10-05T04:40:49.505Z'), + key: 'foo', value: 'barrrrr', + expires: new Date('2096-10-12T04:40:49.000Z'), + domain: 'google.com', + path: '/', + hostOnly: true, + lastAccessed: new Date('2096-10-05T04:40:49.505Z'), }, - ], - headers: [ - { - name: 'Content-Type', - value: 'application/json', + ]; + const cookieJar = await models.cookieJar.getOrCreateForParentId(workspace._id); + await models.cookieJar.update(cookieJar, { + parentId: workspace._id, + cookies, + }); + const request = Object.assign(models.request.init(), { + _id: 'req_123', + parentId: workspace._id, + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], + parameters: [ + { + name: 'foo bar', + value: 'hello&world', + }, + ], + method: 'POST', + body: { + text: 'foo bar', }, - { - name: 'Authorization', - value: 'Basic dXNlcjpwYXNz', + url: 'http://google.com', + authentication: { + type: AUTH_BASIC, + username: 'user', + password: 'pass', }, - ], - headersSize: -1, - httpVersion: 'HTTP/1.1', - method: 'POST', - postData: { - mimeType: '', - params: [], - text: 'foo bar', - }, - queryString: [ - { - name: 'foo bar', - value: 'hello&world', + }); + const { request: renderedRequest } = await render.getRenderedRequestAndContext({ request }); + const har = await harUtils.exportHarWithRequest(renderedRequest); + expect(har.cookies.length).toBe(1); + expect(har).toEqual({ + bodySize: -1, + cookies: [ + { + domain: 'google.com', + expires: '2096-10-12T04:40:49.000Z', + name: 'foo', + path: '/', + value: 'barrrrr', + }, + ], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Authorization', + value: 'Basic dXNlcjpwYXNz', + }, + ], + headersSize: -1, + httpVersion: 'HTTP/1.1', + method: 'POST', + postData: { + mimeType: '', + params: [], + text: 'foo bar', }, - ], - url: 'http://google.com/', - settingEncodeUrl: true, + queryString: [ + { + name: 'foo bar', + value: 'hello&world', + }, + ], + url: 'http://google.com/', + settingEncodeUrl: true, + }); }); - }); - it('export multipart request with file', async () => { - const workspace = await models.workspace.create(); - const request = Object.assign(models.request.init(), { - _id: 'req_123', - parentId: workspace._id, - headers: [ - { - name: 'Content-Type', - value: 'multipart/form-data', - }, - ], - parameters: [], - method: 'POST', - body: { - mimeType: 'multipart/form-data', - params: [ + it('export multipart request with file', async () => { + const workspace = await models.workspace.create(); + const request = Object.assign(models.request.init(), { + _id: 'req_123', + parentId: workspace._id, + headers: [ { - name: 'a_file', - value: '', - fileName: '/tmp/my_file', - type: 'file', - }, - { - name: 'a_simple_field', - value: 'a_simple_value', - }, - { - name: 'a_second_file', - value: '', - fileName: '/tmp/my_file_2', - type: 'file', + name: 'Content-Type', + value: 'multipart/form-data', }, ], - }, - url: 'http://example.com/post', - authentication: {}, - }); - const { request: renderedRequest } = await render.getRenderedRequestAndContext(request); - const har = await harUtils.exportHarWithRequest(renderedRequest); - expect(har).toEqual({ - bodySize: -1, - cookies: [], - headers: [ - { - name: 'Content-Type', - value: 'multipart/form-data', + parameters: [], + method: 'POST', + body: { + mimeType: 'multipart/form-data', + params: [ + { + name: 'a_file', + value: '', + fileName: '/tmp/my_file', + type: 'file', + }, + { + name: 'a_simple_field', + value: 'a_simple_value', + }, + { + name: 'a_second_file', + value: '', + fileName: '/tmp/my_file_2', + type: 'file', + }, + ], }, - ], - headersSize: -1, - httpVersion: 'HTTP/1.1', - method: 'POST', - postData: { - mimeType: 'multipart/form-data', - params: [ + url: 'http://example.com/post', + authentication: {}, + }); + const { request: renderedRequest } = await render.getRenderedRequestAndContext({ request }); + const har = await harUtils.exportHarWithRequest(renderedRequest); + expect(har).toEqual({ + bodySize: -1, + cookies: [], + headers: [ { - name: 'a_file', - fileName: '/tmp/my_file', - }, - { - name: 'a_simple_field', - value: 'a_simple_value', - }, - { - name: 'a_second_file', - fileName: '/tmp/my_file_2', + name: 'Content-Type', + value: 'multipart/form-data', }, ], - text: '', - }, - queryString: [], - url: 'http://example.com/post', - settingEncodeUrl: true, + headersSize: -1, + httpVersion: 'HTTP/1.1', + method: 'POST', + postData: { + mimeType: 'multipart/form-data', + params: [ + { + name: 'a_file', + fileName: '/tmp/my_file', + }, + { + name: 'a_simple_field', + value: 'a_simple_value', + }, + { + name: 'a_second_file', + fileName: '/tmp/my_file_2', + }, + ], + text: '', + }, + queryString: [], + url: 'http://example.com/post', + settingEncodeUrl: true, + }); }); }); }); diff --git a/packages/insomnia-app/app/common/__tests__/render.test.ts b/packages/insomnia-app/app/common/__tests__/render.test.ts index 100e34412a..78ccf1bafa 100644 --- a/packages/insomnia-app/app/common/__tests__/render.test.ts +++ b/packages/insomnia-app/app/common/__tests__/render.test.ts @@ -1,846 +1,745 @@ +import { createBuilder } from '@develohpanda/fluent-builder'; + import { globalBeforeEach } from '../../__jest__/before-each'; import * as models from '../../models'; +import { environmentModelSchema, requestGroupModelSchema } from '../../models/__schemas__/model-schemas'; +import { Environment } from '../../models/environment'; +import { Workspace } from '../../models/workspace'; import * as renderUtils from '../render'; jest.mock('electron'); -describe('render()', () => { - beforeEach(globalBeforeEach); +const envBuilder = createBuilder(environmentModelSchema); +const reqGroupBuilder = createBuilder(requestGroupModelSchema); - it('renders hello world', async () => { - const rendered = await renderUtils.render('Hello {{ msg }}!', { - msg: 'World', - }); - expect(rendered).toBe('Hello World!'); +describe('render tests', () => { + beforeEach(async () => { + await globalBeforeEach(); + await models.space.all(); + envBuilder.reset(); + reqGroupBuilder.reset(); }); - it('renders custom tag: uuid', async () => { - const rendered = await renderUtils.render('Hello {% uuid %}!'); - expect(rendered).toMatch(/Hello [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}!/); - }); - - it('renders custom tag: timestamp', async () => { - const rendered = await renderUtils.render('Hello {% timestamp %}!'); - expect(rendered).toMatch(/Hello \d{13}!/); - }); - - it('renders nested object', async () => { - const rendered = await renderUtils.render('Hello {{ users[0].name }}!', { - users: [ - { - name: 'Niji', - }, - ], - }); - expect(rendered).toBe('Hello Niji!'); - }); - - it('fails on invalid template', async () => { - try { - await renderUtils.render('Hello {{ msg }!', { + describe('render()', () => { + it('renders hello world', async () => { + const rendered = await renderUtils.render('Hello {{ msg }}!', { msg: 'World', }); - fail('Render should have failed'); - } catch (err) { - expect(err.message).toContain('expected variable end'); - } - }); + expect(rendered).toBe('Hello World!'); + }); - it('handles variables using tag before tag is defined as expected (incorrect order)', async () => { - const rootEnvironment = { - type: models.environment.type, - data: { - consume: '{{ replaced }}', - hashed: "{% hash 'md5', 'hex', value %}", - replaced: "{{ hashed | replace('f67565de946a899a534fd908e7eef872', 'cat') }}", + it('renders custom tag: uuid', async () => { + const rendered = await renderUtils.render('Hello {% uuid %}!'); + expect(rendered).toMatch(/Hello [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}!/); + }); + + it('renders custom tag: timestamp', async () => { + const rendered = await renderUtils.render('Hello {% timestamp %}!'); + expect(rendered).toMatch(/Hello \d{13}!/); + }); + + it('renders nested object', async () => { + const rendered = await renderUtils.render('Hello {{ users[0].name }}!', { + users: [ + { + name: 'FooBar', + }, + ], + }); + expect(rendered).toBe('Hello FooBar!'); + }); + + it('fails on invalid template', async () => { + try { + await renderUtils.render('Hello {{ msg }!', { + msg: 'World', + }); + fail('Render should have failed'); + } catch (err) { + expect(err.message).toContain('expected variable end'); + } + }); + + it('handles variables using tag before tag is defined as expected (incorrect order)', async () => { + const rootEnvironment = envBuilder + .data({ + consume: '{{ replaced }}', + hashed: "{% hash 'md5', 'hex', value %}", + replaced: "{{ hashed | replace('f67565de946a899a534fd908e7eef872', 'cat') }}", + value: 'ThisIsATopSecretValue', + }) + .dataPropertyOrder({ + '&': ['value', 'replaced', 'hashed', 'consume'], + }) + .build(); + + const context = await renderUtils.buildRenderContext({ ancestors: [], rootEnvironment }); + expect(context).toEqual({ value: 'ThisIsATopSecretValue', - }, - dataPropertyOrder: { - '&': ['value', 'replaced', 'hashed', 'consume'], - }, - }; - const context = await renderUtils.buildRenderContext([], rootEnvironment); - expect(context).toEqual({ - value: 'ThisIsATopSecretValue', - hashed: 'f67565de946a899a534fd908e7eef872', - replaced: 'f67565de946a899a534fd908e7eef872', - consume: 'f67565de946a899a534fd908e7eef872', + hashed: 'f67565de946a899a534fd908e7eef872', + replaced: 'f67565de946a899a534fd908e7eef872', + consume: 'f67565de946a899a534fd908e7eef872', + }); + // In runtime, this context is used to render, which re-evaluates the expression for replaced in the rootEnvironment by using the built context + // Regression test from issue 1917 - https://github.com/Kong/insomnia/issues/1917 + const renderExpression = await renderUtils.render(rootEnvironment.data.replaced, context); + expect(renderExpression).toBe('cat'); }); - // In runtime, this context is used to render, which re-evaluates the expression for replaced in the rootEnvironment by using the built context - // Regression test from issue 1917 - https://github.com/Kong/insomnia/issues/1917 - const renderExpression = await renderUtils.render(rootEnvironment.data.replaced, context); - expect(renderExpression).toBe('cat'); }); -}); -describe('buildRenderContext()', () => { - beforeEach(globalBeforeEach); + describe('buildRenderContext()', () => { + it('cascades properly', async () => { + const ancestors = [ + reqGroupBuilder.environment({ foo: 'parent', ancestor: true }).build(), + reqGroupBuilder.environment({ foo: 'grandparent', ancestor: true }).build(), + ]; - it('cascades properly', async () => { - const ancestors = [ - { - type: models.requestGroup.type, - environment: { - foo: 'parent', - ancestor: true, - }, - }, - { - type: models.requestGroup.type, - environment: { - foo: 'grandparent', - ancestor: true, - }, - }, - ]; - const rootEnvironment = { - type: models.environment.type, - data: { - foo: 'root', + const rootEnvironment = envBuilder.data({ foo: 'root', root: true }).build(); + const subEnvironment = envBuilder.data({ foo: 'sub', sub: true }).build(); + + const context = await renderUtils.buildRenderContext({ ancestors, rootEnvironment, subEnvironment }); + + expect(context).toEqual({ + foo: 'parent', + ancestor: true, root: true, - }, - }; - const subEnvironment = { - type: models.environment.type, - data: { - foo: 'sub', sub: true, - }, - }; - const context = await renderUtils.buildRenderContext( - ancestors, - rootEnvironment, - subEnvironment, - ); - expect(context).toEqual({ - foo: 'parent', - ancestor: true, - root: true, - sub: true, + }); }); - }); - it('rendered recursive should not infinite loop', async () => { - const ancestors = [ - { - // Sub Environment - type: models.requestGroup.type, - environment: { - recursive: '{{ recursive }}/hello', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - // This is longer than 3 because it multiplies every time (1 -> 2 -> 4 -> 8) - expect(context).toEqual({ - recursive: '{{ recursive }}/hello/hello/hello/hello/hello/hello/hello/hello', + it('rendered recursive should not infinite loop', async () => { + const ancestors = [ + reqGroupBuilder.environment({ recursive: '{{ recursive }}/hello' }).build(), + ]; + + const context = await renderUtils.buildRenderContext({ ancestors }); + // This is longer than 3 because it multiplies every time (1 -> 2 -> 4 -> 8) + expect(context).toEqual({ + recursive: '{{ recursive }}/hello/hello/hello/hello/hello/hello/hello/hello', + }); }); - }); - it('does not recursive render if itself is not used in var', async () => { - const root = { - type: models.environment.type, - data: { + it('does not recursive render if itself is not used in var', async () => { + const root = envBuilder.data({ proto: 'http', domain: 'base.com', url: '{{ proto }}://{{ domain }}', - }, - }; - const sub = { - type: models.environment.type, - data: { + }).build(); + + const sub = envBuilder.data({ proto: 'https', domain: 'sub.com', port: 8000, url: '{{ proto }}://{{ domain }}:{{ port }}', - }, - }; - const ancestors = [ - { - // Folder Environment - type: models.requestGroup.type, - environment: { + }).build(); + + const ancestors = [ + reqGroupBuilder.environment({ proto: 'https', domain: 'folder.com', port: 7000, - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors, root, sub); - expect(context).toEqual({ - proto: 'https', - domain: 'folder.com', - port: 7000, - url: 'https://folder.com:7000', + }).build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors, rootEnvironment: root, subEnvironment: sub }); + expect(context).toEqual({ + proto: 'https', + domain: 'folder.com', + port: 7000, + url: 'https://folder.com:7000', + }); }); - }); - it('does the thing', async () => { - const root = { - type: models.environment.type, - data: { - url: 'insomnia.rest', - }, - }; - const sub = { - type: models.environment.type, - data: { - url: '{{ url }}/sub', - }, - }; - const ancestors = [ - { - // Folder Environment - type: models.requestGroup.type, - environment: { + it('does the thing', async () => { + const root = envBuilder.data({ url: 'insomnia.rest' }).build(); + const sub = envBuilder.data({ url: '{{ url }}/sub' }).build(); + const ancestors = [ + reqGroupBuilder.environment({ url: '{{ url }}/{{ name }}', name: 'folder', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors, root, sub); - expect(context).toEqual({ - url: 'insomnia.rest/sub/folder', - name: 'folder', - }); - }); + }).build(), + ]; - it('render up to 3 recursion levels', async () => { - const ancestors = [ - { - // Sub Environment - type: models.requestGroup.type, - environment: { + const context = await renderUtils.buildRenderContext({ ancestors, rootEnvironment: root, subEnvironment: sub }); + expect(context).toEqual({ + url: 'insomnia.rest/sub/folder', + name: 'folder', + }); + }); + + it('render up to 3 recursion levels', async () => { + const ancestors = [ + reqGroupBuilder.environment({ d: '/d', c: '/c{{ d }}', b: '/b{{ c }}', a: '/a{{ b }}', test: 'http://insomnia.rest{{ a }}', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - d: '/d', - c: '/c/d', - b: '/b/c/d', - a: '/a/b/c/d', - test: 'http://insomnia.rest/a/b/c/d', + }).build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + d: '/d', + c: '/c/d', + b: '/b/c/d', + a: '/a/b/c/d', + test: 'http://insomnia.rest/a/b/c/d', + }); }); - }); - it('rendered sibling environment variables', async () => { - const ancestors = [ - { - // Sub Environment - type: models.requestGroup.type, - environment: { + it('rendered sibling environment variables', async () => { + const ancestors = [ + reqGroupBuilder.environment({ sibling: 'sibling', test: '{{ sibling }}/hello', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - sibling: 'sibling', - test: 'sibling/hello', + }).build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + sibling: 'sibling', + test: 'sibling/hello', + }); }); - }); - it('rendered parent environment variables', async () => { - const ancestors = [ - { - name: 'Parent', - type: models.requestGroup.type, - environment: { - test: '{{ grandparent }} parent', - }, - }, - { - name: 'Grandparent', - type: models.requestGroup.type, - environment: { - grandparent: 'grandparent', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - grandparent: 'grandparent', - test: 'grandparent parent', + it('rendered parent environment variables', async () => { + const ancestors = [ + reqGroupBuilder + .name('Parent') + .environment({ + test: '{{ grandparent }} parent', + }) + .build(), + reqGroupBuilder + .name('Grandparent') + .environment({ + grandparent: 'grandparent', + }) + .build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + grandparent: 'grandparent', + test: 'grandparent parent', + }); }); - }); - it('rendered parent same name environment variables', async () => { - const ancestors = [ - { - name: 'Parent', - type: models.requestGroup.type, - environment: { - base_url: '{{ base_url }}/resource', - }, - }, - { - name: 'Grandparent', - type: models.requestGroup.type, - environment: { - base_url: 'https://insomnia.rest', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - base_url: 'https://insomnia.rest/resource', + it('rendered parent same name environment variables', async () => { + const ancestors = [ + reqGroupBuilder + .name('Parent') + .environment({ + base_url: '{{ base_url }}/resource', + }) + .build(), + reqGroupBuilder + .name('Grandparent') + .environment({ + base_url: 'https://insomnia.rest', + }) + .build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + base_url: 'https://insomnia.rest/resource', + }); }); - }); - it('rendered parent, ignoring sibling environment variables', async () => { - const ancestors = [ - { - name: 'Parent', - type: models.requestGroup.type, - environment: { - host: 'parent.com', - }, - }, - { - name: 'Grandparent', - type: models.requestGroup.type, - environment: { - host: 'grandparent.com', - node: { - admin: 'admin', - test: 'test', - port: 8080, - }, - urls: { - admin: 'https://{{ host }}/{{ node.admin }}', - test: 'https://{{ host }}/{{ node.test }}', - }, - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(await renderUtils.render('{{ urls.admin }}/foo', context)).toBe( - 'https://parent.com/admin/foo', - ); - expect(await renderUtils.render('{{ urls.test }}/foo', context)).toBe( - 'https://parent.com/test/foo', - ); - }); - - it('renders child environment variables', async () => { - const ancestors = [ - { - name: 'Parent', - type: models.requestGroup.type, - environment: { - parent: 'parent', - }, - }, - { - name: 'Grandparent', - type: models.requestGroup.type, - environment: { - test: '{{ parent }} grandparent', - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - parent: 'parent', - test: 'parent grandparent', - }); - }); - - it('works with object arrays', async () => { - const ancestors = [ - { - name: 'Parent', - type: models.requestGroup.type, - environment: {}, - }, - { - name: 'Grandparent', - type: models.requestGroup.type, - environment: { - users: [ - { - name: 'Mike', + it('rendered parent, ignoring sibling environment variables', async () => { + const ancestors = [ + reqGroupBuilder + .name('Parent') + .environment({ + host: 'parent.com', + }) + .build(), + reqGroupBuilder + .name('Grandparent') + .environment({ + host: 'grandparent.com', + node: { + admin: 'admin', + test: 'test', + port: 8080, }, - { - name: 'Opender', + urls: { + admin: 'https://{{ host }}/{{ node.admin }}', + test: 'https://{{ host }}/{{ node.test }}', }, - ], - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - users: [ - { - name: 'Mike', - }, - { - name: 'Opender', - }, - ], + }) + .build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(await renderUtils.render('{{ urls.admin }}/foo', context)).toBe( + 'https://parent.com/admin/foo', + ); + expect(await renderUtils.render('{{ urls.test }}/foo', context)).toBe( + 'https://parent.com/test/foo', + ); }); - }); - it('works with ordered objects', async () => { - const obj = { - users: [ - { - name: 'Mike', - id: 1, - }, - { - name: 'Opender', - id: 2, - }, - ], - }; - const order = { - '&': ['users'], - '&~|users~|0': ['id', 'name'], - '&~|users~|1': ['id', 'name'], - }; - const requestGroup = { - name: 'Parent', - type: models.requestGroup.type, - environment: obj, - environmentPropertyOrder: order, - }; - const rootEnvironment = { - name: 'Parent', - type: models.environment.type, - data: obj, - dataPropertyOrder: order, - }; - const subEnvironment = { - name: 'Sub', - type: models.environment.type, - data: obj, - dataPropertyOrder: order, - }; - const groupCtx = await renderUtils.buildRenderContext([requestGroup]); - const rootCtx = await renderUtils.buildRenderContext([], rootEnvironment); - const subCtx = await renderUtils.buildRenderContext([], null, subEnvironment); - const expected = { - users: [ - { - id: 1, - name: 'Mike', - }, - { - id: 2, - name: 'Opender', - }, - ], - }; - expect(groupCtx).toEqual(expected); - expect(rootCtx).toEqual(expected); - expect(subCtx).toEqual(expected); - }); - - it('merges nested properties when rendering', async () => { - const ancestors = [ - { - name: 'Parent', - type: models.requestGroup.type, - environment: { - parent: 'parent', - nested: { - common: 'parent', - parentA: 'pa', - parentB: 'pb', - }, - }, - }, - { - name: 'Grandparent', - type: models.requestGroup.type, - environment: { - test: '{{ parent }} grandparent', - nested: { - common: 'grandparent', - grandParentA: 'gpa', - grandParentB: 'gpb', - }, - }, - }, - ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - parent: 'parent', - test: 'parent grandparent', - nested: { - common: 'parent', - grandParentA: 'gpa', - grandParentB: 'gpb', - parentA: 'pa', - parentB: 'pb', - }, + it('renders child environment variables', async () => { + const ancestors = [ + reqGroupBuilder + .name('Parent') + .environment({ + parent: 'parent', + }) + .build(), + reqGroupBuilder + .name('Grandparent') + .environment({ + test: '{{ parent }} grandparent', + }) + .build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + parent: 'parent', + test: 'parent grandparent', + }); }); - }); - it('cascades properly and renders', async () => { - const ancestors = [ - { - type: models.requestGroup.type, - environment: { - url: '{{ base_url }}/resource', - ancestor: true, - winner: 'folder parent', + it('works with object arrays', async () => { + const ancestors = [ + reqGroupBuilder + .name('Parent') + .environment({}) + .build(), + reqGroupBuilder + .name('Grandparent') + .environment({ + users: [ + { + name: 'Foo', + }, + { + name: 'Bar', + }, + ], + }) + .build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + users: [ + { + name: 'Foo', + }, + { + name: 'Bar', + }, + ], + }); + }); + + it('works with ordered objects', async () => { + const obj = { + users: [ + { + name: 'Foo', + id: 1, + }, + { + name: 'Bar', + id: 2, + }, + ], + }; + const order = { + '&': ['users'], + '&~|users~|0': ['id', 'name'], + '&~|users~|1': ['id', 'name'], + }; + const requestGroup = reqGroupBuilder.name('Parent').environment(obj).environmentPropertyOrder(order).build(); + const rootEnvironment = envBuilder.name('Parent').data(obj).dataPropertyOrder(order).build(); + const subEnvironment = envBuilder.name('Sub').data(obj).dataPropertyOrder(order).build(); + + const groupCtx = await renderUtils.buildRenderContext({ ancestors: [requestGroup] }); + const rootCtx = await renderUtils.buildRenderContext({ ancestors: [], rootEnvironment }); + const subCtx = await renderUtils.buildRenderContext({ ancestors: [], subEnvironment }); + + const expected = { + users: [ + { + id: 1, + name: 'Foo', + }, + { + id: 2, + name: 'Bar', + }, + ], + }; + expect(groupCtx).toEqual(expected); + expect(rootCtx).toEqual(expected); + expect(subCtx).toEqual(expected); + }); + + it('merges nested properties when rendering', async () => { + const ancestors = [ + reqGroupBuilder + .name('Parent') + .environment({ + parent: 'parent', + nested: { + common: 'parent', + parentA: 'pa', + parentB: 'pb', + }, + }).build(), + reqGroupBuilder + .name('Grandparent') + .environment({ + test: '{{ parent }} grandparent', + nested: { + common: 'grandparent', + grandParentA: 'gpa', + grandParentB: 'gpb', + }, + }).build(), + ]; + const context = await renderUtils.buildRenderContext({ ancestors }); + expect(context).toEqual({ + parent: 'parent', + test: 'parent grandparent', + nested: { + common: 'parent', + grandParentA: 'gpa', + grandParentB: 'gpb', + parentA: 'pa', + parentB: 'pb', }, - }, - { - type: models.requestGroup.type, - environment: { - ancestor: true, - winner: 'folder grandparent', - }, - }, - ]; - const subEnvironment = { - type: models.environment.type, - data: { + }); + }); + + it('cascades properly and renders', async () => { + const ancestors = [ + reqGroupBuilder + .environment({ + url: '{{ base_url }}/resource', + ancestor: true, + winner: 'folder parent', + }) + .build(), + reqGroupBuilder + .environment({ + ancestor: true, + winner: 'folder grandparent', + }) + .build(), + ]; + const subEnvironment = envBuilder.data({ winner: 'sub', sub: true, base_url: 'https://insomnia.rest', - }, - }; - const rootEnvironment = { - type: models.environment.type, - data: { + }).build(); + const rootEnvironment = envBuilder.data({ winner: 'root', root: true, base_url: 'ignore this', - }, - }; - const context = await renderUtils.buildRenderContext( - ancestors, - rootEnvironment, - subEnvironment, - ); - expect(context).toEqual({ - base_url: 'https://insomnia.rest', - url: 'https://insomnia.rest/resource', - ancestor: true, - winner: 'folder parent', - root: true, - sub: true, + }).build(); + const context = await renderUtils.buildRenderContext( + { ancestors, rootEnvironment, subEnvironment }, + ); + expect(context).toEqual({ + base_url: 'https://insomnia.rest', + url: 'https://insomnia.rest/resource', + ancestor: true, + winner: 'folder parent', + root: true, + sub: true, + }); }); - }); - it('handles variables using tag after tag is defined as expected (correct order)', async () => { - const rootEnvironment = { - type: models.environment.type, - data: { - consume: '{{ replaced }}', - hashed: "{% hash 'md5', 'hex', value %}", - replaced: "{{ hashed | replace('f67565de946a899a534fd908e7eef872', 'cat') }}", + it('handles variables using tag after tag is defined as expected (correct order)', async () => { + const rootEnvironment = envBuilder + .data({ + consume: '{{ replaced }}', + hashed: "{% hash 'md5', 'hex', value %}", + replaced: "{{ hashed | replace('f67565de946a899a534fd908e7eef872', 'cat') }}", + value: 'ThisIsATopSecretValue', + }) + .dataPropertyOrder({ + '&': ['value', 'hashed', 'replaced', 'consume'], + }) + .build(); + const context = await renderUtils.buildRenderContext({ ancestors: [], rootEnvironment }); + expect(context).toEqual({ value: 'ThisIsATopSecretValue', - }, - dataPropertyOrder: { - '&': ['value', 'hashed', 'replaced', 'consume'], - }, - }; - const context = await renderUtils.buildRenderContext([], rootEnvironment); - expect(context).toEqual({ - value: 'ThisIsATopSecretValue', - hashed: 'f67565de946a899a534fd908e7eef872', - replaced: 'cat', - consume: 'cat', + hashed: 'f67565de946a899a534fd908e7eef872', + replaced: 'cat', + consume: 'cat', + }); }); - }); - it('handles variables being used in tags', async () => { - const rootEnvironment = { - type: models.environment.type, - data: { - hash_input: '{{ orderId }}{{ secret }}', + it('handles variables being used in tags', async () => { + const rootEnvironment = envBuilder + .data({ + hash_input: '{{ orderId }}{{ secret }}', + hash_input_expected: '123456789012345ThisIsATopSecretValue', + orderId: 123456789012345, + password: "{% hash 'sha512', 'hex', hash_input %}", + password_expected: "{% hash 'sha512', 'hex', hash_input_expected %}", + secret: 'ThisIsATopSecretValue', + }) + .build(); + const context = await renderUtils.buildRenderContext({ ancestors: [], rootEnvironment }); + expect(context).toEqual({ + hash_input: '123456789012345ThisIsATopSecretValue', hash_input_expected: '123456789012345ThisIsATopSecretValue', orderId: 123456789012345, - password: "{% hash 'sha512', 'hex', hash_input %}", - password_expected: "{% hash 'sha512', 'hex', hash_input_expected %}", + password: + 'ea84d15f33d3f9e9098fe01659b1ea0599d345770bba20ba98bf9056676a83ffe6b5528b2451ad04badbf690cf3009a94c510121cc6897045f8bb4ba0826134c', + password_expected: + 'ea84d15f33d3f9e9098fe01659b1ea0599d345770bba20ba98bf9056676a83ffe6b5528b2451ad04badbf690cf3009a94c510121cc6897045f8bb4ba0826134c', secret: 'ThisIsATopSecretValue', - }, - }; - const context = await renderUtils.buildRenderContext([], rootEnvironment); - expect(context).toEqual({ - hash_input: '123456789012345ThisIsATopSecretValue', - hash_input_expected: '123456789012345ThisIsATopSecretValue', - orderId: 123456789012345, - password: - 'ea84d15f33d3f9e9098fe01659b1ea0599d345770bba20ba98bf9056676a83ffe6b5528b2451ad04badbf690cf3009a94c510121cc6897045f8bb4ba0826134c', - password_expected: - 'ea84d15f33d3f9e9098fe01659b1ea0599d345770bba20ba98bf9056676a83ffe6b5528b2451ad04badbf690cf3009a94c510121cc6897045f8bb4ba0826134c', - secret: 'ThisIsATopSecretValue', + }); + }); + + it('works with minimal parameters', async () => { + const context = await renderUtils.buildRenderContext({ }); + expect(context).toEqual({}); }); }); - it('works with minimal parameters', async () => { - const ancestors = null; - const rootEnvironment = null; - const subEnvironment = null; - const context = await renderUtils.buildRenderContext( - ancestors, - rootEnvironment, - subEnvironment, - ); - expect(context).toEqual({}); - }); -}); - -describe('render()', () => { - beforeEach(globalBeforeEach); - - it('correctly renders simple Object', async () => { - const newObj = await renderUtils.render( - { - foo: '{{ foo }}', - bar: 'bar', - baz: '{{ bad }}', - }, - { - foo: 'bar', - bad: 'hi', - }, - ); - expect(newObj).toEqual({ - foo: 'bar', - bar: 'bar', - baz: 'hi', - }); - }); - - it('correctly renders complex Object', async () => { - const d = new Date(); - const obj = { - foo: '{{ foo }}', - null: null, - bool: true, - date: d, - undef: undefined, - num: 1234, - nested: { - foo: '{{ foo }}', - arr: [1, 2, '{{ foo }}'], - }, - }; - const newObj = await renderUtils.render(obj, { - foo: 'bar', - }); - expect(newObj).toEqual({ - foo: 'bar', - null: null, - bool: true, - date: d, - undef: undefined, - num: 1234, - nested: { - foo: 'bar', - arr: [1, 2, 'bar'], - }, - }); - // Make sure original request isn't changed - expect(obj.foo).toBe('{{ foo }}'); - expect(obj.nested.foo).toBe('{{ foo }}'); - expect(obj.nested.arr[2]).toBe('{{ foo }}'); - }); - - it('fails on bad template', async () => { - try { - await renderUtils.render( + describe('render()', () => { + it('correctly renders simple Object', async () => { + const newObj = await renderUtils.render( { - foo: '{{ foo }', + foo: '{{ foo }}', bar: 'bar', baz: '{{ bad }}', }, { foo: 'bar', + bad: 'hi', }, ); - fail('Render should have failed'); - } catch (err) { - expect(err.message).toContain('expected variable end'); - } - }); - - it('keep on error setting', async () => { - const template = '{{ foo }} {% invalid "hi" %}'; - const context = { - foo: 'bar', - }; - const resultOnlyVars = await renderUtils.render( - template, - context, - null, - renderUtils.KEEP_ON_ERROR, - ); - expect(resultOnlyVars).toBe('{{ foo }} {% invalid "hi" %}'); - - try { - await renderUtils.render(template, context, null); - fail('Render should not have succeeded'); - } catch (err) { - expect(err.message).toBe('unknown block tag: invalid'); - } - }); - - it('outputs correct error path', async () => { - const template = { - foo: [ - { - bar: '{% foo %}', - }, - ], - }; - - try { - await renderUtils.render(template); - fail('Should have failed to render'); - } catch (err) { - expect(err.path).toBe('foo[0].bar'); - } - }); - - it('outputs correct error path when private first node', async () => { - const template = { - _foo: { - _bar: { - baz: '{% foo %}', - }, - }, - }; - - try { - await renderUtils.render(template); - fail('Should have failed to render'); - } catch (err) { - expect(err.path).toBe('_bar.baz'); - } - }); -}); - -describe('getRenderedGrpcRequestMessage()', () => { - beforeEach(globalBeforeEach); - - it('renders only the body for a grpc request ', async () => { - const w1 = await models.workspace.create(); - const env = await models.environment.create({ - parentId: w1._id, - data: { + expect(newObj).toEqual({ foo: 'bar', - host: 'testb.in:9000', - }, + bar: 'bar', + baz: 'hi', + }); }); - const grpcRequest = await models.grpcRequest.create({ - parentId: w1._id, - name: 'hi {{ foo }}', - url: '{{ host }}', - description: 'hi {{ foo }}', - body: { - text: '{ "prop": "{{ foo }}" }', - }, - }); - const request = await renderUtils.getRenderedGrpcRequestMessage(grpcRequest, env._id); - expect(request).toEqual( - expect.objectContaining({ - text: '{ "prop": "bar" }', - }), - ); - }); -}); -describe('getRenderedGrpcRequest()', () => { - let w1, env; - beforeEach(async () => { - await globalBeforeEach(); - w1 = await models.workspace.create(); - env = await models.environment.create({ - parentId: w1._id, - data: { - foo: 'bar', - host: 'testb.in:9000', - }, - }); - }); - - it('renders all grpc request properties', async () => { - const grpcRequest = await models.grpcRequest.create({ - parentId: w1._id, - name: 'hi {{ foo }}', - url: '{{ host }}', - description: 'hi {{ foo }}', - body: { - text: '{ "prop": "{{ foo }}" }', - }, - }); - const request = await renderUtils.getRenderedGrpcRequest(grpcRequest, env._id); - expect(request).toEqual( - expect.objectContaining({ - name: 'hi bar', - url: 'testb.in:9000', - description: 'hi bar', - body: { - text: '{ "prop": "bar" }', + it('correctly renders complex Object', async () => { + const d = new Date(); + const obj = { + foo: '{{ foo }}', + null: null, + bool: true, + date: d, + undef: undefined, + num: 1234, + nested: { + foo: '{{ foo }}', + arr: [1, 2, '{{ foo }}'], }, - }), - ); + }; + const newObj = await renderUtils.render(obj, { + foo: 'bar', + }); + expect(newObj).toEqual({ + foo: 'bar', + null: null, + bool: true, + date: d, + undef: undefined, + num: 1234, + nested: { + foo: 'bar', + arr: [1, 2, 'bar'], + }, + }); + // Make sure original request isn't changed + expect(obj.foo).toBe('{{ foo }}'); + expect(obj.nested.foo).toBe('{{ foo }}'); + expect(obj.nested.arr[2]).toBe('{{ foo }}'); + }); + + it('fails on bad template', async () => { + try { + await renderUtils.render( + { + foo: '{{ foo }', + bar: 'bar', + baz: '{{ bad }}', + }, + { + foo: 'bar', + }, + ); + fail('Render should have failed'); + } catch (err) { + expect(err.message).toContain('expected variable end'); + } + }); + + it('keep on error setting', async () => { + const template = '{{ foo }} {% invalid "hi" %}'; + const context = { + foo: 'bar', + }; + const resultOnlyVars = await renderUtils.render( + template, + context, + null, + renderUtils.KEEP_ON_ERROR, + ); + expect(resultOnlyVars).toBe('{{ foo }} {% invalid "hi" %}'); + + try { + await renderUtils.render(template, context, null); + fail('Render should not have succeeded'); + } catch (err) { + expect(err.message).toBe('unknown block tag: invalid'); + } + }); + + it('outputs correct error path', async () => { + const template = { + foo: [ + { + bar: '{% foo %}', + }, + ], + }; + + try { + await renderUtils.render(template); + fail('Should have failed to render'); + } catch (err) { + expect(err.path).toBe('foo[0].bar'); + } + }); + + it('outputs correct error path when private first node', async () => { + const template = { + _foo: { + _bar: { + baz: '{% foo %}', + }, + }, + }; + + try { + await renderUtils.render(template); + fail('Should have failed to render'); + } catch (err) { + expect(err.path).toBe('_bar.baz'); + } + }); }); - it('renders but ignores the body for a grpc request ', async () => { - const grpcRequest = await models.grpcRequest.create({ - parentId: w1._id, - name: 'hi {{ foo }}', - url: '{{ host }}', - description: 'hi {{ foo }}', - body: { - text: '{ "prop": "{{ foo }}" }', - }, - }); - const request = await renderUtils.getRenderedGrpcRequest( - grpcRequest, - env._id, - null, - null, - true, - ); - expect(request).toEqual( - expect.objectContaining({ - name: 'hi bar', - url: 'testb.in:9000', - description: 'hi bar', + describe('getRenderedGrpcRequestMessage()', () => { + it('renders only the body for a grpc request ', async () => { + const w1 = await models.workspace.create(); + const env = await models.environment.create({ + parentId: w1._id, + data: { + foo: 'bar', + host: 'testb.in:9000', + }, + }); + const grpcRequest = await models.grpcRequest.create({ + parentId: w1._id, + name: 'hi {{ foo }}', + url: '{{ host }}', + description: 'hi {{ foo }}', body: { text: '{ "prop": "{{ foo }}" }', }, - }), - ); + }); + const request = await renderUtils.getRenderedGrpcRequestMessage({ request: grpcRequest, environmentId: env._id }); + expect(request).toEqual( + expect.objectContaining({ + text: '{ "prop": "bar" }', + }), + ); + }); }); - it('should still render with bad description', async () => { - const grpcRequest = await models.grpcRequest.create({ - parentId: w1._id, - name: 'hi {{ foo }}', - url: '{{ host }}', - description: 'hi {{ some error }}', - body: { - text: '{ "prop": "{{ foo }}" }', - }, + describe('getRenderedGrpcRequest()', () => { + let w1: Workspace; + let env: Environment; + + beforeEach(async () => { + w1 = await models.workspace.create(); + env = await models.environment.create({ + parentId: w1._id, + data: { + foo: 'bar', + host: 'testb.in:9000', + }, + }); }); - const request = await renderUtils.getRenderedGrpcRequest(grpcRequest, env._id); - expect(request).toEqual( - expect.objectContaining({ - name: 'hi bar', - url: 'testb.in:9000', + + it('renders all grpc request properties', async () => { + const grpcRequest = await models.grpcRequest.create({ + parentId: w1._id, + name: 'hi {{ foo }}', + url: '{{ host }}', + description: 'hi {{ foo }}', + body: { + text: '{ "prop": "{{ foo }}" }', + }, + }); + const request = await renderUtils.getRenderedGrpcRequest({ request: grpcRequest, environmentId: env._id }); + expect(request).toEqual( + expect.objectContaining({ + name: 'hi bar', + url: 'testb.in:9000', + description: 'hi bar', + body: { + text: '{ "prop": "bar" }', + }, + }), + ); + }); + + it('renders but ignores the body for a grpc request ', async () => { + const grpcRequest = await models.grpcRequest.create({ + parentId: w1._id, + name: 'hi {{ foo }}', + url: '{{ host }}', + description: 'hi {{ foo }}', + body: { + text: '{ "prop": "{{ foo }}" }', + }, + }); + const request = await renderUtils.getRenderedGrpcRequest({ request: grpcRequest, environmentId: env._id, skipBody: true }); + expect(request).toEqual( + expect.objectContaining({ + name: 'hi bar', + url: 'testb.in:9000', + description: 'hi bar', + body: { + text: '{ "prop": "{{ foo }}" }', + }, + }), + ); + }); + + it('should still render with bad description', async () => { + const grpcRequest = await models.grpcRequest.create({ + parentId: w1._id, + name: 'hi {{ foo }}', + url: '{{ host }}', description: 'hi {{ some error }}', body: { - text: '{ "prop": "bar" }', + text: '{ "prop": "{{ foo }}" }', }, - }), - ); + }); + const request = await renderUtils.getRenderedGrpcRequest({ request: grpcRequest, environmentId: env._id }); + expect(request).toEqual( + expect.objectContaining({ + name: 'hi bar', + url: 'testb.in:9000', + description: 'hi {{ some error }}', + body: { + text: '{ "prop": "bar" }', + }, + }), + ); + }); }); }); diff --git a/packages/insomnia-app/app/common/database.ts b/packages/insomnia-app/app/common/database.ts index a20f6688af..126a5224c1 100644 --- a/packages/insomnia-app/app/common/database.ts +++ b/packages/insomnia-app/app/common/database.ts @@ -549,7 +549,7 @@ export const database = { let docsToReturn: T[] = doc ? [doc] : []; - async function next(docs: T[]) { + async function next(docs: T[]): Promise { const foundDocs: T[] = []; for (const d of docs) { diff --git a/packages/insomnia-app/app/common/export.ts b/packages/insomnia-app/app/common/export.ts index 87b4c693de..84297d3c96 100644 --- a/packages/insomnia-app/app/common/export.ts +++ b/packages/insomnia-app/app/common/export.ts @@ -48,9 +48,7 @@ export async function exportWorkspacesHAR( workspaces: Workspace[], includePrivateDocs = false, ) { - // regarding `[null]`, see the comment here in `exportWorkspacesData` - const rootDocs = workspaces.length === 0 ? [null] : workspaces; - const promises = rootDocs.map(getDocWithDescendants(includePrivateDocs)); + const promises = workspaces.map(getDocWithDescendants(includePrivateDocs)); const docs = (await Promise.all(promises)).flat(); const requests = docs.filter(isRequest); return exportRequestsHAR(requests, includePrivateDocs); @@ -124,9 +122,7 @@ export async function exportWorkspacesData( includePrivateDocs: boolean, format: 'json' | 'yaml', ) { - // Semantically, if an empty array is passed, then nothing will be returned. What an empty array really signifies is "no parent", which, at the database layer is the same as "parentId === null", hence we add null in ourselves. - const rootDocs = workspaces.length === 0 ? [null] : workspaces; - const promises = rootDocs.map(getDocWithDescendants(includePrivateDocs)); + const promises = workspaces.map(getDocWithDescendants(includePrivateDocs)); const docs = (await Promise.all(promises)).flat(); const requests = docs.filter(doc => isRequest(doc) || isGrpcRequest(doc)); return exportRequestsData(requests, includePrivateDocs, format); diff --git a/packages/insomnia-app/app/common/har.ts b/packages/insomnia-app/app/common/har.ts index 07125d01cd..bdf0c6c3ae 100644 --- a/packages/insomnia-app/app/common/har.ts +++ b/packages/insomnia-app/app/common/har.ts @@ -186,7 +186,7 @@ export async function exportHar(exportRequests: ExportRequest[]) { continue; } - const harRequest = await exportHarWithRequest(request, exportRequest.environmentId); + const harRequest = await exportHarWithRequest(request, exportRequest.environmentId || undefined); if (!harRequest) { continue; @@ -283,11 +283,11 @@ export async function exportHarRequest( export async function exportHarWithRequest( request: Request, - environmentId: string | null, + environmentId?: string, addContentLength = false, ) { try { - const renderResult = await getRenderedRequestAndContext(request, environmentId); + const renderResult = await getRenderedRequestAndContext({ request, environmentId }); const renderedRequest = await _applyRequestPluginHooks( renderResult.request, renderResult.context, diff --git a/packages/insomnia-app/app/common/import.ts b/packages/insomnia-app/app/common/import.ts index 5cb145bc46..52c692d57e 100644 --- a/packages/insomnia-app/app/common/import.ts +++ b/packages/insomnia-app/app/common/import.ts @@ -40,7 +40,7 @@ interface ConvertResult { export interface ImportRawConfig { getWorkspaceId: ImportToWorkspacePrompt; - getSpaceId?: () => Promise; + getSpaceId?: () => Promise; getWorkspaceScope?: SetWorkspaceScopePrompt; enableDiffBasedPatching?: boolean; enableDiffDeep?: boolean; @@ -336,11 +336,10 @@ async function updateWorkspaceScope( async function createWorkspaceInSpace( resource: Workspace, - getSpaceId?: () => Promise, + getSpaceId?: () => Promise, ) { if (getSpaceId) { // Set the workspace parent if creating a new workspace during import - // @ts-expect-error workspace parent can be null or string resource.parentId = await getSpaceId(); } } diff --git a/packages/insomnia-app/app/common/render.ts b/packages/insomnia-app/app/common/render.ts index bb55afbe3e..6ce7c96a6d 100644 --- a/packages/insomnia-app/app/common/render.ts +++ b/packages/insomnia-app/app/common/render.ts @@ -6,10 +6,10 @@ import * as models from '../models'; import type { CookieJar } from '../models/cookie-jar'; import type { Environment } from '../models/environment'; import type { GrpcRequest, GrpcRequestBody } from '../models/grpc-request'; -import type { BaseModel } from '../models/index'; import type { Request } from '../models/request'; -import { isRequestGroup } from '../models/request-group'; -import { isWorkspace } from '../models/workspace'; +import { isRequestGroup, RequestGroup } from '../models/request-group'; +import { isSpace, Space } from '../models/space'; +import { isWorkspace, Workspace } from '../models/workspace'; import * as templating from '../templating'; import * as templatingUtils from '../templating/utils'; import { CONTENT_TYPE_GRAPHQL, JSON_ORDER_SEPARATOR } from './constants'; @@ -54,10 +54,17 @@ export type HandleGetRenderContext = () => Promise; export type HandleRender = (object: T, contextCacheKey?: string | null) => Promise; export async function buildRenderContext( - ancestors: BaseModel[] | null, - rootEnvironment: Environment | null, - subEnvironment: Environment | null, - baseContext: Record = {}, + { + ancestors, + rootEnvironment, + subEnvironment, + baseContext = {}, + }: { + ancestors?: RenderContextAncestor[]; + rootEnvironment?: Environment; + subEnvironment?: Environment; + baseContext?: Record; + }, ) { const envObjects: Record[] = []; @@ -280,17 +287,31 @@ export async function render( return next(newObj, name, true); } -export async function getRenderContext( - request: Request | GrpcRequest | null, - environmentId: string | null, - ancestors: BaseModel[] | null = null, - purpose: RenderPurpose | null = null, - extraInfo: ExtraRenderInfo | null = null, -): Promise> { - if (!ancestors) { - ancestors = await _getRequestAncestors(request); - } +interface RenderRequest { + request: T; +} +interface BaseRenderContextOptions { + environmentId?: string; + purpose?: RenderPurpose; + extraInfo?: ExtraRenderInfo; +} + +interface RenderContextOptions extends BaseRenderContextOptions, Partial> { + ancestors?: RenderContextAncestor[]; +} +export async function getRenderContext( + { + request, + environmentId, + ancestors: _ancestors, + purpose, + extraInfo, + }: RenderContextOptions, +): Promise> { + const ancestors = _ancestors || await getRenderContextAncestors(request); + + const space = ancestors.find(isSpace); const workspace = ancestors.find(isWorkspace); if (!workspace) { @@ -373,24 +394,26 @@ export async function getRenderContext( baseContext.getEnvironmentId = () => environmentId; + // It is possible for a space to not exist because this code path can be reached via Inso/insomnia-send-request which has no concept of a space. + baseContext.getSpaceId = () => space?._id; + // Generate the context we need to render - return buildRenderContext(ancestors, rootEnvironment, subEnvironment, baseContext); + return buildRenderContext({ ancestors, rootEnvironment, subEnvironment: subEnvironment || undefined, baseContext }); } +interface RenderGrpcRequestOptions extends BaseRenderContextOptions, RenderRequest { + skipBody?: boolean; +} export async function getRenderedGrpcRequest( - request: GrpcRequest, - environmentId: string | null, - purpose?: RenderPurpose, - extraInfo?: ExtraRenderInfo, - skipBody?: boolean, -) { - const renderContext = await getRenderContext( + { + purpose, + extraInfo, request, environmentId, - null, - purpose, - extraInfo || null, - ); + skipBody, + }: RenderGrpcRequestOptions, +) { + const renderContext = await getRenderContext({ request, environmentId, purpose, extraInfo }); const description = request.description; // Render description separately because it's lower priority request.description = ''; @@ -406,41 +429,35 @@ export async function getRenderedGrpcRequest( return renderedRequest; } +type RenderGrpcRequestMessageOptions = BaseRenderContextOptions & RenderRequest; export async function getRenderedGrpcRequestMessage( - request: GrpcRequest, - environmentId: string | null, - purpose?: RenderPurpose, - extraInfo?: ExtraRenderInfo, -) { - const renderContext = await getRenderContext( - request, + { environmentId, - null, + request, + extraInfo, purpose, - extraInfo || null, - ); + }: RenderGrpcRequestMessageOptions, +) { + const renderContext = await getRenderContext({ request, environmentId, purpose, extraInfo }); // Render request body const renderedBody: RenderedGrpcRequestBody = await render(request.body, renderContext); return renderedBody; } +type RenderRequestOptions = BaseRenderContextOptions & RenderRequest; export async function getRenderedRequestAndContext( - request: Request, - environmentId?: string | null, - purpose?: RenderPurpose, - extraInfo?: ExtraRenderInfo, + { + request, + environmentId, + extraInfo, + purpose, + }: RenderRequestOptions, ) { - const ancestors = await _getRequestAncestors(request); + const ancestors = await getRenderContextAncestors(request); const workspace = ancestors.find(isWorkspace); const parentId = workspace ? workspace._id : 'n/a'; const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId); - const renderContext = await getRenderContext( - request, - environmentId || null, - ancestors, - purpose, - extraInfo || null, - ); + const renderContext = await getRenderContext({ request, environmentId, ancestors, purpose, extraInfo }); // HACK: Switch '#}' to '# }' to prevent Nunjucks from barfing // https://github.com/kong/insomnia/issues/895 @@ -539,11 +556,13 @@ function _getOrderedEnvironmentKeys(finalRenderContext: Record): st }); } -async function _getRequestAncestors(request: Request | GrpcRequest | null): Promise { - return await db.withAncestors(request, [ +type RenderContextAncestor = Request | GrpcRequest | RequestGroup | Workspace | Space; +export async function getRenderContextAncestors(base?: Request | GrpcRequest | Workspace): Promise { + return await db.withAncestors(base || null, [ models.request.type, models.grpcRequest.type, models.requestGroup.type, models.workspace.type, + models.space.type, ]); } diff --git a/packages/insomnia-app/app/models/__schemas__/model-schemas.ts b/packages/insomnia-app/app/models/__schemas__/model-schemas.ts index 197dc3dea0..bc94989897 100644 --- a/packages/insomnia-app/app/models/__schemas__/model-schemas.ts +++ b/packages/insomnia-app/app/models/__schemas__/model-schemas.ts @@ -1,7 +1,8 @@ import { Schema } from '@develohpanda/fluent-builder'; import clone from 'clone'; -import { BaseModel, gitRepository, grpcRequest, request, requestGroup, workspace } from '..'; +import { BaseModel, environment, gitRepository, grpcRequest, request, requestGroup, workspace } from '..'; +import { Environment } from '../environment'; import { GitRepository } from '../git-repository'; import { GrpcRequest } from '../grpc-request'; import { Request } from '../request'; @@ -64,3 +65,9 @@ export const gitRepositorySchema: Schema = { needsFullClone: () => false, uriNeedsMigration: () => true, }; + +export const environmentModelSchema: Schema = { + ...baseModelSchema, + ...toSchema(environment.init()), + type: () => environment.type, +}; diff --git a/packages/insomnia-app/app/models/space.ts b/packages/insomnia-app/app/models/space.ts index ada489fff4..3da241829e 100644 --- a/packages/insomnia-app/app/models/space.ts +++ b/packages/insomnia-app/app/models/space.ts @@ -1,3 +1,4 @@ +import { getAppName } from '../common/constants'; import { database as db } from '../common/database'; import { generateId } from '../common/misc'; import type { BaseModel } from './index'; @@ -8,15 +9,13 @@ export const prefix = 'sp'; export const canDuplicate = false; export const canSync = false; -// A base space represents "no" space, when viewing this base space fetch all workspaces with no parent. -// Using this instead of null. -export const BASE_SPACE_ID = 'base-space'; +export const BASE_SPACE_ID = `${prefix}_base-space`; -export const isBaseSpace = ({ _id }: Pick) => _id === BASE_SPACE_ID; -export const isNotBaseSpace = (space: Pick) => !isBaseSpace(space); -export const isLocalSpace = ({ remoteId }: Pick) => remoteId === null; -export const isRemoteSpace = (space: Pick) => !isLocalSpace(space); -export const spaceHasSettings = (space: Pick) => isLocalSpace(space) && !isBaseSpace(space); +export const isBaseSpace = ({ _id }: Space) => _id === BASE_SPACE_ID; +export const isNotBaseSpace = (space: Space) => !isBaseSpace(space); +export const isLocalSpace = ({ remoteId }: Space) => remoteId === null; +export const isRemoteSpace = (space: Space) => !isLocalSpace(space); +export const spaceHasSettings = (space: Space) => isLocalSpace(space) && !isBaseSpace(space); interface BaseSpace { name: string; @@ -64,6 +63,13 @@ export function update(space: Space, patch: Partial) { return db.docUpdate(space, patch); } -export function all() { - return db.all(type); +export async function all() { + const spaces = await db.all(type); + + if (!spaces.find(c => c._id === BASE_SPACE_ID)) { + await create({ _id: BASE_SPACE_ID, name: getAppName(), remoteId: null }); + return db.all(type); + } + + return spaces; } diff --git a/packages/insomnia-app/app/models/workspace.ts b/packages/insomnia-app/app/models/workspace.ts index ea6c205d0a..b9e63220f9 100644 --- a/packages/insomnia-app/app/models/workspace.ts +++ b/packages/insomnia-app/app/models/workspace.ts @@ -4,7 +4,7 @@ import { database as db } from '../common/database'; import { strings } from '../common/strings'; import type { BaseModel } from './index'; import * as models from './index'; -import { isSpaceId } from './space'; +import { BASE_SPACE_ID, isSpaceId } from './space'; export const name = 'Workspace'; export const type = 'Workspace'; @@ -57,6 +57,7 @@ export async function migrate(doc: Workspace) { fileName: doc.name, }); doc = _migrateScope(doc); + doc = _migrateIntoBaseSpace(doc); return doc; } @@ -64,6 +65,10 @@ export function getById(id?: string) { return db.get(type, id); } +export function findByParentId(parentId: string) { + return db.find(type, { parentId }); +} + export async function create(patch: Partial = {}) { expectParentToBeSpace(patch.parentId); return db.docCreate(type, patch); @@ -153,6 +158,14 @@ function _migrateScope(workspace: MigrationWorkspace) { return workspace as Workspace; } +function _migrateIntoBaseSpace(workspace: Workspace) { + if (!workspace.parentId) { + workspace.parentId = BASE_SPACE_ID; + } + + return workspace; +} + export async function ensureChildren({ _id }: Workspace) { await models.environment.getOrCreateForParentId(_id); await models.cookieJar.getOrCreateForParentId(_id); diff --git a/packages/insomnia-app/app/network/__tests__/network.test.ts b/packages/insomnia-app/app/network/__tests__/network.test.ts index c36ffb2c9c..1935680883 100644 --- a/packages/insomnia-app/app/network/__tests__/network.test.ts +++ b/packages/insomnia-app/app/network/__tests__/network.test.ts @@ -19,10 +19,13 @@ import { DEFAULT_BOUNDARY } from '../multipart'; import * as networkUtils from '../network'; const CONTEXT = {}; -const getRenderedRequest = async (...args) => (await getRenderedRequestAndContext(...args)).request; +const getRenderedRequest = async (args: Parameters[0]) => (await getRenderedRequestAndContext(args)).request; describe('actuallySend()', () => { - beforeEach(globalBeforeEach); + beforeEach(async () => { + await globalBeforeEach(); + await models.space.all(); + }); it('sends a generic request', async () => { const workspace = await models.workspace.create(); @@ -90,7 +93,7 @@ describe('actuallySend()', () => { password: 'pass', }, }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -165,7 +168,7 @@ describe('actuallySend()', () => { }, url: 'http://localhost', }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -265,7 +268,7 @@ describe('actuallySend()', () => { settingStoreCookies: false, settingSendCookies: false, }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -325,7 +328,7 @@ describe('actuallySend()', () => { fileName, }, }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -405,7 +408,7 @@ describe('actuallySend()', () => { ], }, }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -466,7 +469,7 @@ describe('actuallySend()', () => { url: 'http://unix:/my/socket:/my/path', method: 'GET', }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -506,7 +509,7 @@ describe('actuallySend()', () => { url: 'http://localhost:3000/foo/bar', method: 'HEAD', }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -545,7 +548,7 @@ describe('actuallySend()', () => { url: 'http://unix:3000/my/path', method: 'GET', }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -585,7 +588,7 @@ describe('actuallySend()', () => { type: AUTH_NETRC, }, }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const response = await networkUtils._actuallySend( renderedRequest, CONTEXT, @@ -623,7 +626,7 @@ describe('actuallySend()', () => { _id: 'req_123', parentId: workspace._id, }); - const renderedRequest = await getRenderedRequest(request); + const renderedRequest = await getRenderedRequest({ request }); const responseV1 = await networkUtils._actuallySend(renderedRequest, CONTEXT, workspace, { ...settings, preferredHttpVersion: HttpVersions.V1_0, @@ -674,8 +677,8 @@ describe('actuallySend()', () => { url: 'http://unix:3000/requestB', method: 'GET', }); - const renderedRequest1 = await getRenderedRequest(request1); - const renderedRequest2 = await getRenderedRequest(request2); + const renderedRequest1 = await getRenderedRequest({ request: request1 }); + const renderedRequest2 = await getRenderedRequest({ request: request2 }); // WHEN const response1Promise = networkUtils._actuallySend( diff --git a/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts b/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts index a2e51e86bb..fa28d0bf91 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts +++ b/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts @@ -1,7 +1,9 @@ +import { mocked } from 'ts-jest/utils'; + import { globalBeforeEach } from '../../../__jest__/before-each'; import { - getRenderedGrpcRequest, - getRenderedGrpcRequestMessage, + getRenderedGrpcRequest as _getRenderedGrpcRequest, + getRenderedGrpcRequestMessage as _getRenderedGrpcRequestMessage, RENDER_PURPOSE_SEND, } from '../../../common/render'; import * as models from '../../../models'; @@ -10,6 +12,9 @@ import { prepareGrpcMessage, prepareGrpcRequest } from '../prepare'; jest.mock('../../../common/render'); +const getRenderedGrpcRequest = mocked(_getRenderedGrpcRequest); +const getRenderedGrpcRequestMessage = mocked(_getRenderedGrpcRequestMessage); + describe('prepareGrpcRequest', () => { beforeEach(globalBeforeEach); @@ -26,11 +31,12 @@ describe('prepareGrpcRequest', () => { getRenderedGrpcRequest.mockResolvedValue(gr); const result = await prepareGrpcRequest(gr._id, env._id, methodType); expect(getRenderedGrpcRequest).toHaveBeenLastCalledWith( - gr, - env, - RENDER_PURPOSE_SEND, - {}, - false, + { + request: gr, + environmentId: env._id, + purpose: RENDER_PURPOSE_SEND, + skipBody: false, + }, ); expect(result).toEqual({ request: gr, @@ -51,11 +57,12 @@ describe('prepareGrpcRequest', () => { getRenderedGrpcRequest.mockResolvedValue(gr); const result = await prepareGrpcRequest(gr._id, env._id, methodType); expect(getRenderedGrpcRequest).toHaveBeenLastCalledWith( - gr, - env, - RENDER_PURPOSE_SEND, - {}, - true, + { + request: gr, + environmentId: env._id, + purpose: RENDER_PURPOSE_SEND, + skipBody: true, + }, ); expect(result).toEqual({ request: gr, @@ -78,10 +85,11 @@ describe('prepareGrpcMessage', () => { getRenderedGrpcRequestMessage.mockResolvedValue(gr.body); const result = await prepareGrpcMessage(gr._id, env._id); expect(getRenderedGrpcRequestMessage).toHaveBeenLastCalledWith( - gr, - env, - RENDER_PURPOSE_SEND, - {}, + { + request: gr, + environmentId: env._id, + purpose: RENDER_PURPOSE_SEND, + }, ); expect(result).toEqual({ body: gr.body, diff --git a/packages/insomnia-app/app/network/grpc/prepare.ts b/packages/insomnia-app/app/network/grpc/prepare.ts index 13423529ac..07ca63a7ad 100644 --- a/packages/insomnia-app/app/network/grpc/prepare.ts +++ b/packages/insomnia-app/app/network/grpc/prepare.ts @@ -23,15 +23,16 @@ export const prepareGrpcRequest = async ( methodType: GrpcMethodType, ): Promise => { const req = await models.grpcRequest.getById(requestId); - const environment = await models.environment.getById(environmentId || 'n/a'); const request = await getRenderedGrpcRequest( - // @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null - req, - environment, - RENDER_PURPOSE_SEND, - {}, - canClientStream(methodType), + { + // @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null + request: req, + environmentId, + purpose: RENDER_PURPOSE_SEND, + skipBody: canClientStream(methodType), + }, ); + return { request, }; @@ -42,13 +43,12 @@ export const prepareGrpcMessage = async ( environmentId: string, ): Promise => { const req = await models.grpcRequest.getById(requestId); - const environment = await models.environment.getById(environmentId || 'n/a'); - const requestBody = await getRenderedGrpcRequestMessage( + const requestBody = await getRenderedGrpcRequestMessage({ // @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null - req, - environment, - RENDER_PURPOSE_SEND, - {}, + request: req, + environmentId, + purpose: RENDER_PURPOSE_SEND, + }, ); return { body: requestBody, diff --git a/packages/insomnia-app/app/network/network.ts b/packages/insomnia-app/app/network/network.ts index 0531fbd62f..5d2f12dc5b 100644 --- a/packages/insomnia-app/app/network/network.ts +++ b/packages/insomnia-app/app/network/network.ts @@ -895,7 +895,7 @@ export async function sendWithSettings( }; try { - renderResult = await getRenderedRequestAndContext(newRequest, environmentId); + renderResult = await getRenderedRequestAndContext({ request: newRequest, environmentId }); } catch (err) { throw new Error(`Failed to render request: ${requestId}`); } @@ -946,10 +946,12 @@ export async function send( const environment: Environment | null = await models.environment.getById(environmentId || 'n/a'); const renderResult = await getRenderedRequestAndContext( - request, - environmentId || null, - RENDER_PURPOSE_SEND, - extraInfo, + { + request, + environmentId, + purpose: RENDER_PURPOSE_SEND, + extraInfo, + }, ); const renderedRequestBeforePlugins = renderResult.request; const renderedContextBeforePlugins = renderResult.context; @@ -1004,7 +1006,7 @@ async function _applyRequestPluginHooks( for (const { plugin, hook } of await plugins.getRequestHooks()) { const context = { ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), - ...pluginContexts.data.init(), + ...pluginContexts.data.init(renderedContext.getSpaceId()), ...(pluginContexts.store.init(plugin) as Record), ...(pluginContexts.request.init(newRenderedRequest, renderedContext) as Record), ...(pluginContexts.network.init(renderedContext.getEnvironmentId()) as Record), @@ -1032,7 +1034,7 @@ async function _applyResponsePluginHooks( for (const { plugin, hook } of await plugins.getResponseHooks()) { const context = { ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), - ...pluginContexts.data.init(), + ...pluginContexts.data.init(renderedContext.getSpaceId()), ...(pluginContexts.store.init(plugin) as Record), ...(pluginContexts.response.init(newResponse) as Record), ...(pluginContexts.request.init(newRequest, renderedContext, true) as Record), diff --git a/packages/insomnia-app/app/plugins/context/__tests__/data.test.ts b/packages/insomnia-app/app/plugins/context/__tests__/data.test.ts index 3261b44ee0..2cf7620c7e 100644 --- a/packages/insomnia-app/app/plugins/context/__tests__/data.test.ts +++ b/packages/insomnia-app/app/plugins/context/__tests__/data.test.ts @@ -5,6 +5,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { getAppVersion } from '../../../common/constants'; import { database as db } from '../../../common/database'; import * as models from '../../../models/index'; +import { BASE_SPACE_ID, Space } from '../../../models/space'; import { WorkspaceScopeKeys } from '../../../models/workspace'; import * as plugin from '../data'; @@ -14,7 +15,7 @@ describe('init()', () => { beforeEach(globalBeforeEach); it('initializes correctly', async () => { - const { data } = plugin.init(); + const { data } = plugin.init(BASE_SPACE_ID); expect(Object.keys(data)).toEqual(['import', 'export']); expect(Object.keys(data.export).sort()).toEqual(['har', 'insomnia']); expect(Object.keys(data.import).sort()).toEqual(['raw', 'uri']); @@ -22,12 +23,16 @@ describe('init()', () => { }); describe('app.import.*', () => { + let space: Space; + beforeEach(async () => { await globalBeforeEach(); + space = await models.space.create(); await models.workspace.create({ _id: 'wrk_1', created: 111, modified: 222, + parentId: space._id, }); }); @@ -35,7 +40,7 @@ describe('app.import.*', () => { const workspace = await models.workspace.getById('wrk_1'); expect(await db.all(models.workspace.type)).toEqual([workspace]); expect(await db.count(models.request.type)).toBe(0); - const { data } = plugin.init(); + const { data } = plugin.init(space._id); const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json'); await data.import.uri(`file://${filename}`); const allWorkspaces = await db.all(models.workspace.type); @@ -47,7 +52,7 @@ describe('app.import.*', () => { modified: 999, description: '', name: 'New', - parentId: null, + parentId: space._id, type: 'Workspace', scope: WorkspaceScopeKeys.collection, }, @@ -83,7 +88,7 @@ describe('app.import.*', () => { const workspace = await models.workspace.getById('wrk_1'); expect(await db.all(models.workspace.type)).toEqual([workspace]); expect(await db.count(models.request.type)).toBe(0); - const { data } = plugin.init(); + const { data } = plugin.init(space._id); const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json'); await data.import.raw(fs.readFileSync(filename, 'utf8')); const allWorkspaces = await db.all(models.workspace.type); @@ -95,7 +100,7 @@ describe('app.import.*', () => { modified: 999, description: '', name: 'New', - parentId: null, + parentId: space._id, type: 'Workspace', scope: WorkspaceScopeKeys.collection, }, @@ -129,12 +134,15 @@ describe('app.import.*', () => { }); describe('app.export.*', () => { + let space: Space; beforeEach(async () => { await globalBeforeEach(); + space = await models.space.create(); await models.workspace.create({ _id: 'wrk_1', created: 111, modified: 222, + parentId: space._id, }); await models.request.create({ _id: 'req_1', @@ -153,7 +161,7 @@ describe('app.export.*', () => { }); it('insomnia', async () => { - const { data } = plugin.init(); + const { data } = plugin.init(space._id); const exported = await data.export.insomnia(); const exportedData = JSON.parse(exported); expect(typeof exportedData.__export_date).toBe('string'); @@ -202,7 +210,7 @@ describe('app.export.*', () => { }); it('har', async () => { - const { data } = plugin.init(); + const { data } = plugin.init(space._id); const exported = await data.export.har(); const exportedData = JSON.parse(exported); exportedData.log.entries[0].startedDateTime = '2017-11-24T18:12:12.849Z'; diff --git a/packages/insomnia-app/app/plugins/context/data.ts b/packages/insomnia-app/app/plugins/context/data.ts index 0fe55e92ff..319fe16e36 100644 --- a/packages/insomnia-app/app/plugins/context/data.ts +++ b/packages/insomnia-app/app/plugins/context/data.ts @@ -1,6 +1,8 @@ import { exportWorkspacesData, exportWorkspacesHAR } from '../../common/export'; import type { ImportRawConfig } from '../../common/import'; import { importRaw, importUri } from '../../common/import'; +import * as models from '../../models'; +import { BASE_SPACE_ID } from '../../models/space'; import type { Workspace, WorkspaceScope } from '../../models/workspace'; interface PluginImportOptions { @@ -16,21 +18,29 @@ interface InsomniaExport { type HarExport = Omit; -const buildImportRawConfig = (options: PluginImportOptions): ImportRawConfig => ({ +const buildImportRawConfig = (options: PluginImportOptions, activeSpaceId: string): ImportRawConfig => ({ getWorkspaceId: () => Promise.resolve(options.workspaceId || null), getWorkspaceScope: options.scope && (() => ( Promise.resolve(options.scope as WorkspaceScope)) ), + getSpaceId: () => Promise.resolve(activeSpaceId), }); -export const init = () => ({ +// TODO: add metrics here to track how frequently this fallback is being used +const getWorkspaces = (activeSpaceId?: string) => + activeSpaceId + ? models.workspace.findByParentId(activeSpaceId) + : models.workspace.all(); + +// Only in the case of running unit tests from Inso via send-request can activeSpaceId be undefined. This is because the concept of a space doesn't exist in git/insomnia sync or an export file +export const init = (activeSpaceId?: string) => ({ data: { import: { uri: async (uri: string, options: PluginImportOptions = {}) => { - await importUri(uri, buildImportRawConfig(options)); + await importUri(uri, buildImportRawConfig(options, activeSpaceId || BASE_SPACE_ID)); }, raw: async (text: string, options: PluginImportOptions = {}) => { - await importRaw(text, buildImportRawConfig(options)); + await importRaw(text, buildImportRawConfig(options, activeSpaceId || BASE_SPACE_ID)); }, }, export: { @@ -39,7 +49,7 @@ export const init = () => ({ includePrivate, format, }: InsomniaExport = {}) => exportWorkspacesData( - workspace ? [workspace] : [], + workspace ? [workspace] : await getWorkspaces(activeSpaceId), Boolean(includePrivate), format || 'json', ), @@ -48,7 +58,7 @@ export const init = () => ({ workspace, includePrivate, }: HarExport = {}) => exportWorkspacesHAR( - workspace ? [workspace] : [], + workspace ? [workspace] : await getWorkspaces(activeSpaceId), Boolean(includePrivate), ), }, diff --git a/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts b/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts index fd99d49b43..38813b0e6a 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts +++ b/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts @@ -6,6 +6,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { database as db } from '../../../common/database'; import * as models from '../../../models'; import { workspaceModelSchema } from '../../../models/__schemas__/model-schemas'; +import { BASE_SPACE_ID } from '../../../models/space'; import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../git-vcs'; import { NeDBClient } from '../ne-db-client'; import { assertAsyncError, setupDateMocks } from './util'; @@ -19,6 +20,7 @@ describe('NeDBClient', () => { workspaceBuilder.reset(); setupDateMocks(); // Create some sample models + await models.space.all(); await models.workspace.create({ _id: 'wrk_1', }); @@ -40,7 +42,7 @@ describe('NeDBClient', () => { describe('readdir()', () => { it('reads model IDs from model type folders', async () => { - const neDbClient = new NeDBClient('wrk_1'); + const neDbClient = new NeDBClient('wrk_1', BASE_SPACE_ID); const reqDir = path.join(GIT_INSOMNIA_DIR, models.request.type); const wrkDir = path.join(GIT_INSOMNIA_DIR, models.workspace.type); expect(await neDbClient.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]); @@ -66,11 +68,11 @@ describe('NeDBClient', () => { const wrk1Yml = path.join(GIT_INSOMNIA_DIR, models.workspace.type, 'wrk_1.yml'); const req1Yml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_1.yml'); const reqXYml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_x.yml'); - const pNeDB = new NeDBClient('wrk_1'); + const pNeDB = new NeDBClient('wrk_1', BASE_SPACE_ID); expect(YAML.parse((await pNeDB.readFile(wrk1Yml, 'utf8')).toString())).toEqual( expect.objectContaining({ _id: 'wrk_1', - parentId: null, + parentId: null, // should be reset to default value instead of the active space id }), ); expect(YAML.parse((await pNeDB.readFile(req1Yml, 'utf8')).toString())).toEqual( @@ -95,7 +97,7 @@ describe('NeDBClient', () => { type: 'file', }); // Act - const neDbClient = new NeDBClient('wrk_1'); + const neDbClient = new NeDBClient('wrk_1', BASE_SPACE_ID); // Assert expect(await neDbClient.stat(GIT_CLONE_DIR)).toEqual(dirType); expect(await neDbClient.stat(GIT_INSOMNIA_DIR)).toEqual(dirType); @@ -110,7 +112,7 @@ describe('NeDBClient', () => { // Arrange const upsertSpy = jest.spyOn(db, 'upsert'); const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId); + const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID); const env = { _id: 'env_1', type: models.environment.type, @@ -128,7 +130,7 @@ describe('NeDBClient', () => { it('should write files in GIT_INSOMNIA_DIR directory to db', async () => { // Arrange const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId); + const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID); const upsertSpy = jest.spyOn(db, 'upsert'); const env = { _id: 'env_1', @@ -202,7 +204,7 @@ describe('NeDBClient', () => { it('should throw error if id does not match', async () => { // Arrange const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId); + const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID); const env = { _id: 'env_1', type: models.environment.type, @@ -220,7 +222,7 @@ describe('NeDBClient', () => { it('should throw error if type does not match', async () => { // Arrange const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId); + const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID); const env = { _id: 'env_1', type: models.environment.type, @@ -239,7 +241,7 @@ describe('NeDBClient', () => { describe('mkdir()', () => { it('should throw error', async () => { const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId); + const neDbClient = new NeDBClient(workspaceId, BASE_SPACE_ID); const promiseResult = neDbClient.mkdir(); await expect(promiseResult).rejects.toThrowError('NeDBClient is not writable'); }); diff --git a/packages/insomnia-app/app/sync/git/ne-db-client.ts b/packages/insomnia-app/app/sync/git/ne-db-client.ts index c2f9af04ec..5bab9b9c9d 100644 --- a/packages/insomnia-app/app/sync/git/ne-db-client.ts +++ b/packages/insomnia-app/app/sync/git/ne-db-client.ts @@ -16,18 +16,18 @@ import { BufferEncoding } from './utils'; export class NeDBClient { _workspaceId: string; - _spaceId: string | null; + _spaceId: string; - constructor(workspaceId: string, spaceId?: string) { + constructor(workspaceId: string, spaceId: string) { if (!workspaceId) { throw new Error('Cannot use NeDBClient without workspace ID'); } this._workspaceId = workspaceId; - this._spaceId = spaceId || null; + this._spaceId = spaceId; } - static createClient(workspaceId: string, spaceId?: string): PromiseFsClient { + static createClient(workspaceId: string, spaceId: string): PromiseFsClient { return { promises: new NeDBClient(workspaceId, spaceId), }; @@ -104,7 +104,6 @@ export class NeDBClient { // Whenever we write a workspace into nedb we should set the parentId to be that of the current space // This is because the parentId (or a space) is not synced into git, so it will be cleared whenever git writes the workspace into the db, thereby removing it from the space on the client // In order to reproduce this bug, comment out the following line, then clone a repository into a local space, then open the workspace, you'll notice it will have moved into the base space - // @ts-expect-error parentId can be string or null for a workspace doc.parentId = this._spaceId; } diff --git a/packages/insomnia-app/app/sync/vcs/__tests__/pull-project.test.ts b/packages/insomnia-app/app/sync/vcs/__tests__/pull-project.test.ts index bc00155d8b..f1411f118e 100644 --- a/packages/insomnia-app/app/sync/vcs/__tests__/pull-project.test.ts +++ b/packages/insomnia-app/app/sync/vcs/__tests__/pull-project.test.ts @@ -39,25 +39,6 @@ describe('pullProject()', () => { expect(vcs.pull).not.toHaveBeenCalledWith([], expect.anything()); }); - it('should insert a workspace with no parent', async () => { - // Arrange - const space = undefined; - - // Act - await pullProject({ vcs, project, space }); - - // Assert - const workspaces = await models.workspace.all(); - expect(workspaces).toHaveLength(1); - const workspace = workspaces[0]; - expect(workspace).toStrictEqual(expect.objectContaining>({ - _id: project.rootDocumentId, - name: project.name, - // @ts-expect-error parent id is optional for workspaces - parentId: null, - })); - }); - it('should insert a workspace with parent', async () => { // Arrange const space = await models.space.create(); @@ -104,36 +85,6 @@ describe('pullProject()', () => { afterEach(() => { }); - it('should overwrite the parentId only for a workspace with null', async () => { - // Arrange - const space = undefined; - const existingWrk = await models.workspace.create({ _id: project.rootDocumentId, name: project.name }); - const existingReq = await models.request.create({ parentId: existingWrk._id }); - - vcs.allDocuments.mockResolvedValue([existingWrk, existingReq]); - - // Act - await pullProject({ vcs, project, space }); - - // Assert - const workspaces = await models.workspace.all(); - expect(workspaces).toHaveLength(1); - const workspace = workspaces[0]; - expect(workspace).toStrictEqual(expect.objectContaining>({ - _id: project.rootDocumentId, - name: project.name, - // @ts-expect-error parent id is optional for workspaces - parentId: null, - })); - - const requests = await models.request.all(); - expect(requests).toHaveLength(1); - const request = requests[0]; - expect(request).toStrictEqual(existingReq); - - expect(vcs.pull).toHaveBeenCalledWith([], undefined); - }); - it('should overwrite the parentId only for a workspace with the space id', async () => { // Arrange const space = await models.space.create({ remoteId: 'abc' }); diff --git a/packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts b/packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts index b9eae17fc7..1d5bc2c6cc 100644 --- a/packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts +++ b/packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts @@ -943,7 +943,7 @@ describe('util', () => { it('shouldnt change the hash of a workspace after a parent id is added and ignored', () => { // Arrange - // @ts-expect-error parent id should be nullable + // @ts-expect-error parentId is intentionally null, because that's what the data was originally const originalSyncedWorkspace = workspaceModelBuilder.parentId(null).build(); // Existing synced workspaces do not have a modified field diff --git a/packages/insomnia-app/app/sync/vcs/pull-project.ts b/packages/insomnia-app/app/sync/vcs/pull-project.ts index 1a899cfbf3..97fc1e9b00 100644 --- a/packages/insomnia-app/app/sync/vcs/pull-project.ts +++ b/packages/insomnia-app/app/sync/vcs/pull-project.ts @@ -10,7 +10,7 @@ import { VCS } from './vcs'; interface Options { vcs: VCS; project: Project; - space?: Space; + space: Space; } export const pullProject = async ({ vcs, project, space }: Options) => { @@ -20,7 +20,7 @@ export const pullProject = async ({ vcs, project, space }: Options) => { const remoteBranches = await vcs.getRemoteBranches(); const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME); - const workspaceParentId = space?._id || null; + const workspaceParentId = space._id; // The default branch does not exist, so we create it and the workspace locally if (defaultBranchMissing) { @@ -31,14 +31,13 @@ export const pullProject = async ({ vcs, project, space }: Options) => { }); await database.upsert(workspace); } else { - await vcs.pull([], space?.remoteId); // There won't be any existing docs since it's a new pull + await vcs.pull([], space.remoteId); // There won't be any existing docs since it's a new pull const flushId = await database.bufferChanges(); // @ts-expect-error -- TSCONVERSION for (const doc of (await vcs.allDocuments() || [])) { if (isWorkspace(doc)) { - // @ts-expect-error parent id is optional for workspaces doc.parentId = workspaceParentId; } await database.upsert(doc); diff --git a/packages/insomnia-app/app/ui/components/dropdowns/request-actions-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/request-actions-dropdown.tsx index 3be19d0339..f50a312cf0 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/request-actions-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/request-actions-dropdown.tsx @@ -12,6 +12,7 @@ import { GrpcRequest } from '../../../models/grpc-request'; import * as requestOperations from '../../../models/helpers/request-operations'; import { isRequest, Request } from '../../../models/request'; import type { RequestGroup } from '../../../models/request-group'; +import { Space } from '../../../models/space'; import { incrementDeletedRequests } from '../../../models/stats'; // Plugin action related imports import type { RequestAction } from '../../../plugins'; @@ -38,6 +39,7 @@ interface Props extends Partial { hotKeyRegistry: HotKeyRegistry; handleSetRequestPinned: Function; activeEnvironment?: Environment | null; + activeSpace: Space; } // Setup state for plugin actions @@ -101,11 +103,11 @@ class RequestActionsDropdown extends PureComponent { })); try { - const { activeEnvironment, request, requestGroup } = this.props; + const { activeEnvironment, activeSpace, request, requestGroup } = this.props; const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null; const context = { ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), - ...pluginContexts.data.init(), + ...pluginContexts.data.init(activeSpace._id), ...(pluginContexts.store.init(p.plugin) as Record), ...(pluginContexts.network.init(activeEnvironmentId) as Record), }; diff --git a/packages/insomnia-app/app/ui/components/dropdowns/request-group-actions-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/request-group-actions-dropdown.tsx index b33015eb78..06ca86a9fa 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/request-group-actions-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/request-group-actions-dropdown.tsx @@ -9,6 +9,7 @@ import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render'; import * as models from '../../../models'; import type { Environment } from '../../../models/environment'; import type { RequestGroup } from '../../../models/request-group'; +import { Space } from '../../../models/space'; import type { Workspace } from '../../../models/workspace'; import type { RequestGroupAction } from '../../../plugins'; import { getRequestGroupActions } from '../../../plugins'; @@ -25,6 +26,7 @@ import { showError, showModal } from '../modals'; import EnvironmentEditModal from '../modals/environment-edit-modal'; interface Props { + space: Space; workspace: Workspace; requestGroup: RequestGroup; hotKeyRegistry: HotKeyRegistry; @@ -91,11 +93,11 @@ class RequestGroupActionsDropdown extends PureComponent { })); try { - const { activeEnvironment, requestGroup } = this.props; + const { activeEnvironment, requestGroup, space } = this.props; const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null; const context = { ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), - ...pluginContexts.data.init(), + ...pluginContexts.data.init(space._id), ...(pluginContexts.store.init(p.plugin) as Record), ...(pluginContexts.network.init(activeEnvironmentId) as Record), }; diff --git a/packages/insomnia-app/app/ui/components/dropdowns/space-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/space-dropdown.tsx index 1722010a40..176989dd3c 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/space-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/space-dropdown.tsx @@ -3,9 +3,8 @@ import React, { FC, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; -import { getAppName } from '../../../common/constants'; import { strings } from '../../../common/strings'; -import { BASE_SPACE_ID, isBaseSpace, isRemoteSpace, Space, spaceHasSettings } from '../../../models/space'; +import { isBaseSpace, isNotBaseSpace, isRemoteSpace, Space, spaceHasSettings } from '../../../models/space'; import { VCS } from '../../../sync/vcs/vcs'; import { useRemoteSpaces } from '../../hooks/space'; import { setActiveSpace } from '../../redux/modules/global'; @@ -14,14 +13,6 @@ import { selectActiveSpace, selectSpaces } from '../../redux/selectors'; import { showModal } from '../modals'; import SpaceSettingsModal from '../modals/space-settings-modal'; -type SpaceSubset = Pick; - -const baseSpace: SpaceSubset = { - _id: BASE_SPACE_ID, - name: getAppName(), - remoteId: null, -}; - const svgPlacementHack = { // This is a bit of a hack/workaround to avoid some larger changes that we'd need to do with dropdown item icons and tooltips. // Without this, the icon is too high with respect to the text because of Tooltip introducing some changes to the placement of the icon. @@ -67,7 +58,7 @@ interface Props { } const SpaceDropdownItem: FC<{ - space: SpaceSubset; + space: Space; isActive: boolean; setActive: (spaceId: string) => void; }> = ({ isActive, space, setActive }) => { @@ -92,10 +83,9 @@ SpaceDropdownItem.displayName = DropdownItem.name; // This is required because t export const SpaceDropdown: FC = ({ vcs }) => { const { loading, refresh } = useRemoteSpaces(vcs); - // get list of spaces (which doesn't include the base space) const spaces = useSelector(selectSpaces); - const activeSpace = useSelector(selectActiveSpace) || baseSpace; + const activeSpace = useSelector(selectActiveSpace); const dispatch = useDispatch(); const setActive = useCallback((spaceId: string) => dispatch(setActiveSpace(spaceId)), [dispatch]); const createNew = useCallback(() => dispatch(createSpace()), [dispatch]); @@ -109,25 +99,20 @@ export const SpaceDropdown: FC = ({ vcs }) => { ), [activeSpace]); + const renderSpace = useCallback((space: Space) => ( + + ), [setActive, activeSpace._id]); + return ( - - + {spaces.filter(isBaseSpace).map(renderSpace)} All spaces{' '}{loading && spinner} - - {spaces.map(space => ( - - ))} - + {spaces.filter(isNotBaseSpace).map(renderSpace)} {spaceHasSettings(activeSpace) && <> } onClick={showSettings}> @@ -142,3 +127,4 @@ export const SpaceDropdown: FC = ({ vcs }) => { ); }; + diff --git a/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx index 56c7e99284..9130028e98 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx @@ -9,6 +9,7 @@ import * as models from '../../../models'; import type { ApiSpec } from '../../../models/api-spec'; import getWorkspaceName from '../../../models/helpers/get-workspace-name'; import * as workspaceOperations from '../../../models/helpers/workspace-operations'; +import { Space } from '../../../models/space'; import type { Workspace } from '../../../models/workspace'; import { WorkspaceScopeKeys } from '../../../models/workspace'; import type { DocumentAction } from '../../../plugins'; @@ -23,11 +24,12 @@ import AskModal from '../modals/ask-modal'; interface Props { workspace: Workspace; apiSpec: ApiSpec; + space: Space; } const spinner = ; -const useWorkspaceHandlers = ({ workspace, apiSpec }: { workspace: Workspace; apiSpec: ApiSpec; }) => { +const useWorkspaceHandlers = ({ workspace, apiSpec }: Props) => { const dispatch = useDispatch(); const handleDuplicate = useCallback(() => { @@ -78,7 +80,7 @@ const useWorkspaceHandlers = ({ workspace, apiSpec }: { workspace: Workspace; ap return { handleDelete, handleDuplicate, handleRename }; }; -const useDocumentActionPlugins = ({ workspace, apiSpec }: { workspace: Workspace; apiSpec: ApiSpec; }) => { +const useDocumentActionPlugins = ({ workspace, apiSpec, space }: Props) => { const [actionPlugins, setActionPlugins] = useState([]); const { startLoading, stopLoading, isLoading } = useLoadingRecord(); @@ -95,7 +97,7 @@ const useDocumentActionPlugins = ({ workspace, apiSpec }: { workspace: Workspace try { const context = { ...pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER), - ...pluginContexts.data.init(), + ...pluginContexts.data.init(space._id), ...pluginContexts.store.init(p.plugin), }; // @ts-expect-error -- TSCONVERSION @@ -108,7 +110,7 @@ const useDocumentActionPlugins = ({ workspace, apiSpec }: { workspace: Workspace } finally { stopLoading(p.label); } - }, [apiSpec.contents, startLoading, stopLoading]); + }, [apiSpec.contents, space._id, startLoading, stopLoading]); const renderPluginDropdownItems = useCallback(() => actionPlugins.map(p => ( { this.setState(state => ({ loadingActions: { ...state.loadingActions, [p.label]: true }, })); - const { activeEnvironment, activeWorkspace } = this.props; + const { activeEnvironment, activeWorkspace, activeSpace } = this.props; try { const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null; const context = { ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), - ...pluginContexts.data.init(), + ...pluginContexts.data.init(activeSpace._id), ...(pluginContexts.store.init(p.plugin) as Record), ...(pluginContexts.network.init(activeEnvironmentId) as Record), }; diff --git a/packages/insomnia-app/app/ui/components/modals/alert-modal.tsx b/packages/insomnia-app/app/ui/components/modals/alert-modal.tsx index 31485c337e..4d1ba3d412 100644 --- a/packages/insomnia-app/app/ui/components/modals/alert-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/alert-modal.tsx @@ -1,5 +1,5 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactNode } from 'react'; import { AUTOBIND_CFG } from '../../../common/constants'; import Modal from '../base/modal'; @@ -7,13 +7,16 @@ import ModalBody from '../base/modal-body'; import ModalFooter from '../base/modal-footer'; import ModalHeader from '../base/modal-header'; -interface State { - title: string; - message: string; - addCancel: boolean; - okLabel: string; +export interface AlertModalOptions { + title?: string; + message?: ReactNode; + addCancel?: boolean; + okLabel?: string; + onConfirm?: () => void | Promise; } +type State = Omit; + @autoBindMethodsForReact(AUTOBIND_CFG) class AlertModal extends PureComponent<{}, State> { state: State = { @@ -26,8 +29,9 @@ class AlertModal extends PureComponent<{}, State> { modal: Modal | null = null; _cancel: HTMLButtonElement | null = null; _ok: HTMLButtonElement | null = null; - _okCallback: Function | null = null; - _okCallback2: Function | null = null; + + _okCallback: (value: void | PromiseLike) => void; + _okCallback2: AlertModalOptions['onConfirm']; _setModalRef(m: Modal) { this.modal = m; @@ -36,7 +40,7 @@ class AlertModal extends PureComponent<{}, State> { _handleOk() { this.hide(); - this._okCallback?.(); + this._okCallback(); if (typeof this._okCallback2 === 'function') { this._okCallback2(); @@ -55,9 +59,7 @@ class AlertModal extends PureComponent<{}, State> { this._ok = n; } - show(options = {}) { - // @ts-expect-error -- TSCONVERSION - const { title, message, addCancel, onConfirm, okLabel } = options; + show({ title, message, addCancel, onConfirm, okLabel }: AlertModalOptions) { this.setState({ title, message, @@ -70,7 +72,7 @@ class AlertModal extends PureComponent<{}, State> { this._cancel && this._cancel.focus(); }, 100); this._okCallback2 = onConfirm; - return new Promise(resolve => { + return new Promise(resolve => { this._okCallback = resolve; }); } diff --git a/packages/insomnia-app/app/ui/components/modals/index.ts b/packages/insomnia-app/app/ui/components/modals/index.ts index 7ee7d50138..5d2c26cb87 100644 --- a/packages/insomnia-app/app/ui/components/modals/index.ts +++ b/packages/insomnia-app/app/ui/components/modals/index.ts @@ -1,7 +1,6 @@ import { trackEvent } from '../../../common/analytics'; -import AlertModal from './alert-modal'; -import type { ErrorModalOptions } from './error-modal'; -import ErrorModal from './error-modal'; +import AlertModal, { AlertModalOptions } from './alert-modal'; +import ErrorModal, { ErrorModalOptions } from './error-modal'; import PromptModal, { PromptModalOptions } from './prompt-modal'; const modals = {}; @@ -24,7 +23,7 @@ export function showPrompt(config: PromptModalOptions) { return showModal(PromptModal, config); } -export function showAlert(config) { +export function showAlert(config: AlertModalOptions) { return showModal(AlertModal, config); } diff --git a/packages/insomnia-app/app/ui/components/modals/space-settings-modal.tsx b/packages/insomnia-app/app/ui/components/modals/space-settings-modal.tsx index 7ad7031aca..f05df8878c 100644 --- a/packages/insomnia-app/app/ui/components/modals/space-settings-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/space-settings-modal.tsx @@ -6,7 +6,8 @@ import { bindActionCreators } from 'redux'; import { AUTOBIND_CFG } from '../../../common/constants'; import { strings } from '../../../common/strings'; import * as models from '../../../models/index'; -import { Space } from '../../../models/space'; +import { isBaseSpace, isRemoteSpace } from '../../../models/space'; +import { RootState } from '../../redux/modules'; import * as spaceActions from '../../redux/modules/space'; import { selectActiveSpace } from '../../redux/selectors'; import DebouncedInput from '../base/debounced-input'; @@ -15,15 +16,9 @@ import ModalBody from '../base/modal-body'; import ModalHeader from '../base/modal-header'; import PromptButton from '../base/prompt-button'; -interface ReduxStateProps { - space?: Space; -} +export type ReduxProps = ReturnType & ReturnType; -interface ReduxDispatchProps { - handleRemoveSpace: (space: Space) => void; -} - -interface Props extends ReduxStateProps, ReduxDispatchProps { } +type Props = ReduxProps @autoBindMethodsForReact(AUTOBIND_CFG) class SpaceSettingsModal extends PureComponent { @@ -34,32 +29,26 @@ class SpaceSettingsModal extends PureComponent { } _handleRemoveSpace() { - if (this.props.space) { - this.props.handleRemoveSpace(this.props.space); - } - + this.props.handleRemoveSpace(this.props.space); this.hide(); } async _handleRename(name: string) { const { space } = this.props; - - if (space) { - await models.space.update(space, { name }); - } + await models.space.update(space, { name }); } show() { - this.modal && this.modal.show(); + this.modal?.show(); } hide() { - this.modal && this.modal.hide(); + this.modal?.hide(); } render() { const { space } = this.props; - if (!space) { + if (isBaseSpace(space) || isRemoteSpace(space)) { return null; } @@ -98,12 +87,12 @@ class SpaceSettingsModal extends PureComponent { } } -const mapStateToProps = (state): ReduxStateProps => { +const mapStateToProps = (state: RootState) => { const space = selectActiveSpace(state); return { space }; }; -const mapDispatchToProps = (dispatch): ReduxDispatchProps => { +const mapDispatchToProps = dispatch => { const boundSpaceActions = bindActionCreators(spaceActions, dispatch); return { handleRemoveSpace: boundSpaceActions.removeSpace, diff --git a/packages/insomnia-app/app/ui/components/modals/sync-branches-modal.tsx b/packages/insomnia-app/app/ui/components/modals/sync-branches-modal.tsx index 3fd495272e..c304d46d54 100644 --- a/packages/insomnia-app/app/ui/components/modals/sync-branches-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/sync-branches-modal.tsx @@ -16,7 +16,7 @@ import SyncPullButton from '../sync-pull-button'; interface Props { workspace: Workspace; - space?: Space; + space: Space; syncItems: StatusCandidate[]; vcs: VCS; } diff --git a/packages/insomnia-app/app/ui/components/sidebar/sidebar-children.tsx b/packages/insomnia-app/app/ui/components/sidebar/sidebar-children.tsx index 6213dbad8d..d0aafc9170 100644 --- a/packages/insomnia-app/app/ui/components/sidebar/sidebar-children.tsx +++ b/packages/insomnia-app/app/ui/components/sidebar/sidebar-children.tsx @@ -9,6 +9,7 @@ import type { Environment } from '../../../models/environment'; import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request'; import { isRequest, Request } from '../../../models/request'; import type { RequestGroup } from '../../../models/request-group'; +import { Space } from '../../../models/space'; import type { Workspace } from '../../../models/workspace'; import SidebarCreateDropdown from './sidebar-create-dropdown'; import SidebarRequestGroupRow from './sidebar-request-group-row'; @@ -43,6 +44,7 @@ interface Props { hotKeyRegistry: HotKeyRegistry; activeEnvironment?: Environment | null; activeRequest?: Request | GrpcRequest | null; + activeSpace: Space; } @autoBindMethodsForReact(AUTOBIND_CFG) @@ -91,6 +93,7 @@ class SidebarChildren extends PureComponent { workspace, hotKeyRegistry, activeEnvironment, + activeSpace, } = this.props; const activeRequestId = activeRequest ? activeRequest._id : 'n/a'; return children.map(child => { @@ -117,6 +120,7 @@ class SidebarChildren extends PureComponent { request={child.doc} hotKeyRegistry={hotKeyRegistry} // Necessary for plugin actions on requests activeEnvironment={activeEnvironment} + activeSpace={activeSpace} /> ); } @@ -158,6 +162,7 @@ class SidebarChildren extends PureComponent { requestGroup={requestGroup} hotKeyRegistry={hotKeyRegistry} activeEnvironment={activeEnvironment} + activeSpace={activeSpace} > {children} diff --git a/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-group-row.tsx b/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-group-row.tsx index fd8f37bfeb..793154a8c6 100644 --- a/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-group-row.tsx +++ b/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-group-row.tsx @@ -10,6 +10,7 @@ import * as misc from '../../../common/misc'; import { HandleRender } from '../../../common/render'; import { Environment } from '../../../models/environment'; import { RequestGroup } from '../../../models/request-group'; +import { Space } from '../../../models/space'; import { Workspace } from '../../../models/workspace'; import Highlight from '../base/highlight'; import RequestGroupActionsDropdown from '../dropdowns/request-group-actions-dropdown'; @@ -36,6 +37,7 @@ interface Props { connectDragSource?: Function; connectDropTarget?: Function; activeEnvironment?: Environment | null; + activeSpace: Space; } interface State { @@ -107,6 +109,7 @@ class SidebarRequestGroupRow extends PureComponent { workspace, hotKeyRegistry, activeEnvironment, + activeSpace, } = this.props; const { dragDirection } = this.state; let folderIconClass = 'fa-folder'; @@ -192,6 +195,7 @@ class SidebarRequestGroupRow extends PureComponent { hotKeyRegistry={hotKeyRegistry} isPinned={false} // Necessary so that plugin actions work activeEnvironment={activeEnvironment} + activeSpace={activeSpace} /> )} diff --git a/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-row.tsx b/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-row.tsx index aebe6049e8..db09a3e4a9 100644 --- a/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-row.tsx +++ b/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-row.tsx @@ -13,6 +13,7 @@ import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request'; import * as requestOperations from '../../../models/helpers/request-operations'; import { Request } from '../../../models/request'; import { RequestGroup } from '../../../models/request-group'; +import { Space } from '../../../models/space'; import Editable from '../base/editable'; import Highlight from '../base/highlight'; import RequestActionsDropdown from '../dropdowns/request-actions-dropdown'; @@ -23,6 +24,7 @@ import GrpcTag from '../tags/grpc-tag'; import MethodTag from '../tags/method-tag'; interface Props { + activeSpace: Space; activeEnvironment?: Environment | null; handleActivateRequest: Function; handleSetRequestPinned: Function; @@ -212,6 +214,7 @@ class SidebarRequestRow extends PureComponent { isPinned, request, requestGroup, + activeSpace, } = this.props; const { dragDirection } = this.state; let node; @@ -283,6 +286,7 @@ class SidebarRequestRow extends PureComponent { requestGroup={requestGroup} hotKeyRegistry={hotKeyRegistry} // Necessary for plugin actions to have network capabilities activeEnvironment={activeEnvironment} + activeSpace={activeSpace} /> {isPinned && ( diff --git a/packages/insomnia-app/app/ui/components/sync-pull-button.tsx b/packages/insomnia-app/app/ui/components/sync-pull-button.tsx index 5301aa325a..296703f3fa 100644 --- a/packages/insomnia-app/app/ui/components/sync-pull-button.tsx +++ b/packages/insomnia-app/app/ui/components/sync-pull-button.tsx @@ -9,7 +9,7 @@ import { showError } from './modals'; interface Props { vcs: VCS; branch: string; - space?: Space; + space: Space; onPull: (...args: any[]) => any; disabled?: boolean; className?: string; @@ -40,7 +40,7 @@ class SyncPullButton extends PureComponent { try { // Clone old VCS so we don't mess anything up while working on other projects await newVCS.checkout([], branch); - await newVCS.pull([], space?.remoteId); + await newVCS.pull([], space.remoteId); } catch (err) { showError({ title: 'Pull Error', diff --git a/packages/insomnia-app/app/ui/components/workspace-page-header.tsx b/packages/insomnia-app/app/ui/components/workspace-page-header.tsx index 39aeec1a32..75c8f43ff2 100644 --- a/packages/insomnia-app/app/ui/components/workspace-page-header.tsx +++ b/packages/insomnia-app/app/ui/components/workspace-page-header.tsx @@ -23,6 +23,7 @@ const WorkspacePageHeader: FunctionComponent = ({ wrapperProps: { activeApiSpec, activeWorkspace, + activeSpace, activeEnvironment, settings, activity, @@ -47,6 +48,7 @@ const WorkspacePageHeader: FunctionComponent = ({ activeEnvironment={activeEnvironment} activeWorkspace={activeWorkspace} activeApiSpec={activeApiSpec} + activeSpace={activeSpace} hotKeyRegistry={settings.hotKeyRegistry} isLoading={isLoading} /> diff --git a/packages/insomnia-app/app/ui/components/wrapper-debug.tsx b/packages/insomnia-app/app/ui/components/wrapper-debug.tsx index 8a9909fdc9..eafbe638ce 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-debug.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-debug.tsx @@ -108,6 +108,7 @@ class WrapperDebug extends PureComponent { handleSidebarSort, } = this.props; const { + activeSpace, activeEnvironment, activeRequest, activeWorkspace, @@ -185,6 +186,7 @@ class WrapperDebug extends PureComponent { filter={sidebarFilter || ''} hotKeyRegistry={settings.hotKeyRegistry} activeEnvironment={activeEnvironment} + activeSpace={activeSpace} /> ); diff --git a/packages/insomnia-app/app/ui/components/wrapper-home.tsx b/packages/insomnia-app/app/ui/components/wrapper-home.tsx index f46fc6778d..dcbec0c5ff 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-home.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-home.tsx @@ -160,6 +160,7 @@ class WrapperHome extends PureComponent { renderCard(workspace: Workspace) { const { + activeSpace, apiSpecs, workspaceMetas, } = this.props.wrapperProps; @@ -232,7 +233,7 @@ class WrapperHome extends PureComponent { ); } - const docMenu = ; + const docMenu = ; const version = spec?.info?.version || ''; let label: string = strings.collection.singular; let format = ''; diff --git a/packages/insomnia-app/app/ui/containers/app.tsx b/packages/insomnia-app/app/ui/containers/app.tsx index bca1d57982..55a41e2b9f 100644 --- a/packages/insomnia-app/app/ui/containers/app.tsx +++ b/packages/insomnia-app/app/ui/containers/app.tsx @@ -529,13 +529,12 @@ class App extends PureComponent { async _fetchRenderContext() { const { activeEnvironment, activeRequest, activeWorkspace } = this.props; - const environmentId = activeEnvironment ? activeEnvironment._id : null; - const ancestors = await db.withAncestors(activeRequest || activeWorkspace || null, [ - models.request.type, - models.requestGroup.type, - models.workspace.type, - ]); - return render.getRenderContext(activeRequest, environmentId, ancestors); + const ancestors = await render.getRenderContextAncestors(activeRequest || activeWorkspace); + return render.getRenderContext({ + request: activeRequest || undefined, + environmentId: activeEnvironment?._id, + ancestors, + }); } async _handleGetRenderContext(): Promise { @@ -1208,7 +1207,7 @@ class App extends PureComponent { ); /** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */ - const neDbClient = NeDBClient.createClient(activeWorkspace._id, activeSpace?._id); + const neDbClient = NeDBClient.createClient(activeWorkspace._id, activeSpace._id); /** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */ const gitDataClient = fsClient(baseDir); diff --git a/packages/insomnia-app/app/ui/hooks/__tests__/space.test.ts b/packages/insomnia-app/app/ui/hooks/__tests__/space.test.ts index d35155f223..5c59597df5 100644 --- a/packages/insomnia-app/app/ui/hooks/__tests__/space.test.ts +++ b/packages/insomnia-app/app/ui/hooks/__tests__/space.test.ts @@ -4,7 +4,7 @@ import { mocked } from 'ts-jest/utils'; import { globalBeforeEach } from '../../../__jest__/before-each'; import { isLoggedIn as _isLoggedIn } from '../../../account/session'; import * as models from '../../../models'; -import { Space } from '../../../models/space'; +import { BASE_SPACE_ID, Space } from '../../../models/space'; import MemoryDriver from '../../../sync/store/drivers/memory-driver'; import { VCS } from '../../../sync/vcs/vcs'; import { useRemoteSpaces } from '../space'; @@ -36,7 +36,7 @@ describe('useRemoteSpaces', () => { expect(vcs.teams).not.toHaveBeenCalled(); expect(result.current.loading).toBe(false); - await expect(models.space.all()).resolves.toHaveLength(0); + await expect(models.space.all()).resolves.toHaveLength(1); }); it('should load teams each time VCS changes', async () => { @@ -66,8 +66,11 @@ describe('useRemoteSpaces', () => { expect(vcs2.teams).toHaveBeenCalledTimes(1); const allSpaces = await models.space.all(); - expect(allSpaces).toHaveLength(2); + expect(allSpaces).toHaveLength(3); expect(allSpaces).toEqual(expect.arrayContaining([ + expect.objectContaining>({ + _id: BASE_SPACE_ID, + }), expect.objectContaining>({ remoteId: team1.id, name: team1.name, @@ -92,7 +95,7 @@ describe('useRemoteSpaces', () => { await waitFor(() => result.current.loading === false); expect(vcs.teams).toHaveBeenCalledTimes(1); - await expect(models.space.all()).resolves.toHaveLength(0); + await expect(models.space.all()).resolves.toHaveLength(1); const team1 = { id: 'id1', name: 'team1' }; const team2 = { id: 'id2', name: 'team2' }; @@ -105,8 +108,11 @@ describe('useRemoteSpaces', () => { expect(vcs.teams).toHaveBeenCalledTimes(3); const allSpaces = await models.space.all(); - expect(allSpaces).toHaveLength(2); + expect(allSpaces).toHaveLength(3); expect(allSpaces).toEqual(expect.arrayContaining([ + expect.objectContaining>({ + _id: BASE_SPACE_ID, + }), expect.objectContaining>({ remoteId: team1.id, name: team1.name, diff --git a/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts b/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts index c17ad779eb..810c490ed7 100644 --- a/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts +++ b/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts @@ -1,6 +1,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { reduxStateForTest } from '../../../__jest__/redux-state-for-test'; import * as models from '../../../models'; +import { BASE_SPACE_ID, Space } from '../../../models/space'; import { selectActiveSpace } from '../selectors'; describe('selectors', () => { @@ -19,7 +20,7 @@ describe('selectors', () => { expect(space).toStrictEqual(spaceA); }); - it('should return undefined if active space not found', async () => { + it('should return base space if active space not found', async () => { // create two spaces await models.space.create(); await models.space.create(); @@ -28,19 +29,19 @@ describe('selectors', () => { const state = await reduxStateForTest({ activeSpaceId: 'some-other-space' }); const space = selectActiveSpace(state); - expect(space).toBeUndefined(); + expect(space).toStrictEqual(expect.objectContaining>({ _id: BASE_SPACE_ID })); }); - it('should return undefined if no active space', async () => { + it('should return base space if no active space', async () => { // create two spaces await models.space.create(); await models.space.create(); - // set first as selected - const state = await reduxStateForTest({ activeSpaceId: null }); + // set base as selected + const state = await reduxStateForTest({ activeSpaceId: undefined }); const space = selectActiveSpace(state); - expect(space).toBeUndefined(); + expect(space).toStrictEqual(expect.objectContaining>({ _id: BASE_SPACE_ID })); }); }); }); diff --git a/packages/insomnia-app/app/ui/redux/modules/__tests__/workspace.test.ts b/packages/insomnia-app/app/ui/redux/modules/__tests__/workspace.test.ts index af77c793b5..b6bb1e0534 100644 --- a/packages/insomnia-app/app/ui/redux/modules/__tests__/workspace.test.ts +++ b/packages/insomnia-app/app/ui/redux/modules/__tests__/workspace.test.ts @@ -10,6 +10,7 @@ import * as models from '../../../../models'; import { ApiSpec } from '../../../../models/api-spec'; import { CookieJar } from '../../../../models/cookie-jar'; import { Environment } from '../../../../models/environment'; +import { BASE_SPACE_ID } from '../../../../models/space'; import { Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../../../../models/workspace'; import { WorkspaceMeta } from '../../../../models/workspace-meta'; import { getAndClearShowPromptMockArgs } from '../../../../test-utils'; @@ -31,7 +32,7 @@ const createStoreWithSpace = async () => { return { store, space }; }; -const expectedModelsCreated = async (name: string, scope: WorkspaceScope, parentId: string | null = null) => { +const expectedModelsCreated = async (name: string, scope: WorkspaceScope, parentId: string) => { const workspaces = await models.workspace.all(); expect(workspaces).toHaveLength(1); const workspace = workspaces[0]; @@ -40,7 +41,6 @@ const expectedModelsCreated = async (name: string, scope: WorkspaceScope, parent expect(descendents).toHaveLength(5); expect(descendents).toStrictEqual(expect.arrayContaining([ - // @ts-expect-error parentId is nullable for workspaces expect.objectContaining>({ name, scope, parentId, type: models.workspace.type }), expect.objectContaining>({ parentId: workspace._id, type: models.apiSpec.type }), expect.objectContaining>({ parentId: workspace._id, type: models.environment.type }), @@ -125,7 +125,7 @@ describe('workspace', () => { const workspaceName = 'name'; await onComplete?.(workspaceName); - const workspaceId = await expectedModelsCreated(workspaceName, WorkspaceScopeKeys.collection); + const workspaceId = await expectedModelsCreated(workspaceName, WorkspaceScopeKeys.collection, BASE_SPACE_ID); expect(trackSegmentEvent).toHaveBeenCalledWith('Collection Created'); expect(trackEvent).toHaveBeenCalledWith('Workspace', 'Create'); diff --git a/packages/insomnia-app/app/ui/redux/modules/global.tsx b/packages/insomnia-app/app/ui/redux/modules/global.tsx index 9b10c8b364..703013a755 100644 --- a/packages/insomnia-app/app/ui/redux/modules/global.tsx +++ b/packages/insomnia-app/app/ui/redux/modules/global.tsx @@ -39,7 +39,7 @@ import install from '../../../plugins/install'; import { setTheme } from '../../../plugins/misc'; import AskModal from '../../../ui/components/modals/ask-modal'; import AlertModal from '../../components/modals/alert-modal'; -import { showError, showModal } from '../../components/modals/index'; +import { showAlert, showError, showModal } from '../../components/modals/index'; import LoginModal from '../../components/modals/login-modal'; import PaymentNotificationModal from '../../components/modals/payment-notification-modal'; import { SelectModal } from '../../components/modals/select-modal'; @@ -47,7 +47,7 @@ import SettingsModal, { TAB_INDEX_PLUGINS, TAB_INDEX_THEMES, } from '../../components/modals/settings-modal'; -import { selectSettings, selectWorkspacesForActiveSpace } from '../selectors'; +import { selectActiveSpaceName, selectSettings, selectWorkspacesForActiveSpace } from '../selectors'; import { importUri } from './import'; export const LOCALSTORAGE_PREFIX = 'insomnia::meta'; @@ -79,7 +79,7 @@ function activeActivityReducer(state: string | null = null, action) { } } -function activeSpaceReducer(state: string | null = BASE_SPACE_ID, action) { +function activeSpaceReducer(state: string = BASE_SPACE_ID, action) { switch (action.type) { case SET_ACTIVE_SPACE: return action.spaceId; @@ -141,7 +141,7 @@ function loginStateChangeReducer(state = false, action) { export interface GlobalState { isLoading: boolean; - activeSpaceId: string | null; + activeSpaceId: string; activeWorkspaceId: string | null; activeActivity: GlobalActivity | null, isLoggedIn: boolean; @@ -461,10 +461,22 @@ const writeExportedFileToFileSystem = (filename: string, jsonData: string, onDon export const exportAllToFile = () => async (dispatch: Dispatch, getState) => { dispatch(loadStart()); + const state = getState(); + const activeSpaceName = selectActiveSpaceName(state); + const workspaces = selectWorkspacesForActiveSpace(state); + + if (!workspaces.length) { + dispatch(loadStop()); + showAlert({ + title: 'Cannot export', + message: <>There are no workspaces to export in the {activeSpaceName} space., + }); + return; + } + showSelectExportTypeModal({ onCancel: () => { dispatch(loadStop()); }, onDone: async selectedFormat => { - const state = getState(); // Check if we want to export private environments. const environments = await models.environment.all(); @@ -487,8 +499,6 @@ export const exportAllToFile = () => async (dispatch: Dispatch, getState) => { return; } - const workspaces = selectWorkspacesForActiveSpace(state); - let stringifiedExport; try { diff --git a/packages/insomnia-app/app/ui/redux/modules/helpers.ts b/packages/insomnia-app/app/ui/redux/modules/helpers.ts index 1aad91be29..1293d3f6dd 100644 --- a/packages/insomnia-app/app/ui/redux/modules/helpers.ts +++ b/packages/insomnia-app/app/ui/redux/modules/helpers.ts @@ -1,5 +1,4 @@ -import { getAppName } from '../../../common/constants'; -import { BASE_SPACE_ID, Space } from '../../../models/space'; +import { Space } from '../../../models/space'; import { WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace'; import { showModal } from '../../components/modals'; import AskModal from '../../components/modals/ask-modal'; @@ -64,18 +63,17 @@ export function askToSetWorkspaceScope(scope?: WorkspaceScope): SetWorkspaceScop }; } -export type SetSpaceIdPrompt = () => Promise; -export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[]; activeSpace?: Space; }): SetSpaceIdPrompt { +export type SetSpaceIdPrompt = () => Promise; +export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[]; activeSpace: Space; }): SetSpaceIdPrompt { return function() { return new Promise(resolve => { - // If no spaces exist, return null (indicating no parent/space) - if (spaces.length === 0) { - return resolve(null); + // If only one space exists, return that + if (spaces.length === 1) { + return resolve(spaces[0]._id); } - const options = [{ name: getAppName(), value: BASE_SPACE_ID }, ...spaces.map(space => ({ name: space.name, value: space._id }))]; - - const defaultValue = activeSpace?._id || options[0].value; + const options = spaces.map(space => ({ name: space.name, value: space._id })); + const defaultValue = activeSpace._id; showSelectModal({ title: 'Import', @@ -84,7 +82,8 @@ export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[]; value: defaultValue, noEscape: true, onDone: selectedSpaceId => { - resolve(selectedSpaceId === BASE_SPACE_ID ? null : selectedSpaceId); + // @ts-expect-error onDone can send null as an argument; why/how? + resolve(selectedSpaceId); }, }); }); diff --git a/packages/insomnia-app/app/ui/redux/modules/import.ts b/packages/insomnia-app/app/ui/redux/modules/import.ts index 89481969df..1001e3f6dc 100644 --- a/packages/insomnia-app/app/ui/redux/modules/import.ts +++ b/packages/insomnia-app/app/ui/redux/modules/import.ts @@ -50,14 +50,13 @@ const convertToRawConfig = ({ }: ImportOptions, state: RootState): ImportRawConfig => { const activeSpace = selectActiveSpace(state); - // WARNING: this selector does not include the base space const spaces = selectSpaces(state); return ({ getWorkspaceScope: askToSetWorkspaceScope(forceToScope), getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace }), // Currently, just return the active space instead of prompting for which space to import into - getSpaceId: forceToSpace === 'prompt' ? askToImportIntoSpace({ spaces, activeSpace }) : () => Promise.resolve(activeSpace?._id || null), + getSpaceId: forceToSpace === 'prompt' ? askToImportIntoSpace({ spaces, activeSpace }) : () => Promise.resolve(activeSpace._id), }); }; diff --git a/packages/insomnia-app/app/ui/redux/modules/workspace.ts b/packages/insomnia-app/app/ui/redux/modules/workspace.ts index ae161e2352..31d62e05f0 100644 --- a/packages/insomnia-app/app/ui/redux/modules/workspace.ts +++ b/packages/insomnia-app/app/ui/redux/modules/workspace.ts @@ -41,7 +41,6 @@ export const createWorkspace = ({ scope, onCreate }: { }) => { return (dispatch, getState) => { const activeSpace = selectActiveSpace(getState()); - const parentId = activeSpace?._id || null; const design = isDesign({ scope, @@ -61,8 +60,7 @@ export const createWorkspace = ({ scope, onCreate }: { { name, scope, - // @ts-expect-error TSCONVERSION the common parentId isn't typed correctly - parentId, + parentId: activeSpace._id, }, onCreate, ), diff --git a/packages/insomnia-app/app/ui/redux/selectors.ts b/packages/insomnia-app/app/ui/redux/selectors.ts index bc8f18381a..532b12fc7e 100644 --- a/packages/insomnia-app/app/ui/redux/selectors.ts +++ b/packages/insomnia-app/app/ui/redux/selectors.ts @@ -7,6 +7,7 @@ import { BaseModel } from '../../models'; import { getStatusCandidates } from '../../models/helpers/get-status-candidates'; import { isRequest, Request } from '../../models/request'; import { isRequestGroup, RequestGroup } from '../../models/request-group'; +import { BASE_SPACE_ID } from '../../models/space'; import { UnitTestResult } from '../../models/unit-test-result'; import { RootState } from './modules'; @@ -60,7 +61,6 @@ export const selectSettings = createSelector( selectEntitiesLists, entities => entities.settings[0] || models.settings.init()); -/** WARNING: this does not include the base space */ export const selectSpaces = createSelector( selectEntitiesLists, entities => entities.spaces, @@ -70,7 +70,7 @@ export const selectActiveSpace = createSelector( selectEntities, (state: RootState) => state.global.activeSpaceId, (entities, activeSpaceId) => { - return activeSpaceId ? entities.spaces[activeSpaceId] : undefined; + return entities.spaces[activeSpaceId] || entities.spaces[BASE_SPACE_ID]; }, ); @@ -82,10 +82,7 @@ export const selectAllWorkspaces = createSelector( export const selectWorkspacesForActiveSpace = createSelector( selectAllWorkspaces, selectActiveSpace, - (workspaces, activeSpace) => { - const parentId = activeSpace?._id || null; - return workspaces.filter(w => w.parentId === parentId); - }, + (workspaces, activeSpace) => workspaces.filter(w => w.parentId === activeSpace._id), ); export const selectActiveWorkspace = createSelector( @@ -366,7 +363,7 @@ export const selectActiveUnitTests = createSelector( export const selectActiveSpaceName = createSelector( selectActiveSpace, - activeSpace => activeSpace?.name, + activeSpace => activeSpace.name, ); export const selectActiveUnitTestSuites = createSelector(