Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions application/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
// cross-cutting application-level ports (NetworkClient, TemplatingEngine, Clock,
// IdGenerator, ...). Depends on domain only.
// Populated incrementally, one aggregate at a time.
export { Insomnia, type InsomniaDependencies } from './insomnia';
export { renameWorkspace } from './workspace/rename-workspace.use-case';
48 changes: 48 additions & 0 deletions application/src/insomnia.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Workspace, WorkspaceRepository } from 'insomnia-domain';
import { describe, expect, it } from 'vitest';

import { Insomnia } from './insomnia';

function createFakeWorkspaceRepository(seed: Workspace[] = []): WorkspaceRepository {
const store = new Map(seed.map(w => [w._id, w]));
return {
async findById(id) {
return store.get(id) ?? null;
},
async findByProjectId(projectId) {
return [...store.values()].filter(w => w.parentId === projectId);
},
async save(workspace) {
store.set(workspace._id, workspace);
},
async delete(id) {
store.delete(id);
},
};
}

const buildWorkspace = (overrides: Partial<Workspace> = {}): Workspace => ({
_id: 'wrk_1',
type: 'Workspace',
parentId: 'proj_1',
created: 0,
modified: 0,
isPrivate: false,
name: 'Original',
description: '',
scope: 'collection',
...overrides,
});

describe('Insomnia', () => {
it('workspace.renameById() delegates to the injected WorkspaceRepository', async () => {
const workspace = buildWorkspace();
const workspaceRepository = createFakeWorkspaceRepository([workspace]);
const insomnia = new Insomnia({ workspaceRepository });

const renamed = await insomnia.workspace.renameById(workspace._id, 'Renamed');

expect(renamed.name).toBe('Renamed');
expect((await workspaceRepository.findById(workspace._id))?.name).toBe('Renamed');
});
});
27 changes: 27 additions & 0 deletions application/src/insomnia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { WorkspaceRepository } from 'insomnia-domain';

import { WorkspaceModule } from './workspace/workspace.module';

/**
* Concrete infrastructure adapters this app-level facade needs. Each app's own bootstrap code
* constructs these (which repository implementation, which secret-storage adapter, etc.) and
* passes them in here - this interface only names *what* is needed, never a concrete class, so
* `application` stays dependency-free of `infrastructure`.
*/
export interface InsomniaDependencies {
workspaceRepository: WorkspaceRepository;
}

/**
* The single entry point apps use to reach application use-cases, namespaced by aggregate
* (`insomnia.workspace`, `insomnia.request`, ...). One instance is constructed once, in each
* app's own bootstrap code, and threaded through from there (e.g. via React Router context) -
* never constructed per-call or scattered through routes/commands.
*/
export class Insomnia {
workspace: WorkspaceModule;

constructor(dependencies: InsomniaDependencies) {
this.workspace = new WorkspaceModule(dependencies.workspaceRepository);
}
}
11 changes: 11 additions & 0 deletions application/src/workspace/workspace.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { WorkspaceRepository } from 'insomnia-domain';

import { renameWorkspace } from './rename-workspace.use-case';

export class WorkspaceModule {
constructor(private readonly workspaceRepository: WorkspaceRepository) {}

renameById(id: string, name: string) {
return renameWorkspace(this.workspaceRepository, id, name);
}
}
23 changes: 16 additions & 7 deletions apps/desktop/src/common/application-bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// Wires concrete infrastructure adapters to application use-cases. Per the architecture plan,
// this kind of wiring belongs only in each app's own bootstrap code, never scattered through
// routes/commands. Location here is provisional - where each app's bootstrap code should live is
// still an open decision; this file exists to have exactly one place doing this wiring today.
import { renameWorkspace as renameWorkspaceUseCase } from 'application';
// Wires concrete infrastructure adapters to the application-layer Insomnia facade. Per the
// architecture plan, this kind of wiring belongs only in each app's own bootstrap code, never
// scattered through routes/commands. Location here is provisional - where each app's bootstrap
// code should live is still an open decision; this file exists to have exactly one place doing
// this wiring today.
//
// The React Router context token (createContext) lives here rather than in `application`:
// react-router is a desktop-specific framework dependency, and `application` must stay usable by
// any app (apps/cli, future MCP/web apps) - only the Insomnia class itself belongs there.
import { Insomnia } from 'application';
import { nedbWorkspaceRepository } from 'infrastructure';
import { createContext } from 'react-router';

export const renameWorkspace = (workspaceId: string, name: string) =>
renameWorkspaceUseCase(nedbWorkspaceRepository, workspaceId, name);
export const insomnia = new Insomnia({
workspaceRepository: nedbWorkspaceRepository,
});

export const InsomniaContext = createContext<Insomnia>();
4 changes: 3 additions & 1 deletion apps/desktop/src/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { configureFetch } from 'insomnia-api';
import { initDatabase, initServices, services } from 'insomnia-data';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { RouterContextProvider } from 'react-router';
import { HydratedRouter } from 'react-router/dom';

import { insomniaFetch } from '~/common/insomnia-fetch';
Expand All @@ -18,6 +19,7 @@ import { createServicesProxy } from '~/ui/services-proxy';
import { clearOAuthWindowSessionId } from '~/ui/spawn-oauth-window';
import { getInitialEntry } from '~/ui/utils/router';

import { insomnia, InsomniaContext } from './common/application-bootstrap';
import { configureV3ClientDefaults } from './common/configure-v3-client';
import { getInsomniaSession, getInsomniaVaultKey, getInsomniaVaultSalt, getSkipOnboarding } from './common/constants';
import { HtmlElementWrapper } from './ui/components/html-element-wrapper';
Expand Down Expand Up @@ -156,7 +158,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter getContext={() => new RouterContextProvider(new Map([[InsomniaContext, insomnia]]))} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { models, services } from 'insomnia-data';
import { href } from 'react-router';

import { renameWorkspace } from '~/common/application-bootstrap';
import { InsomniaContext } from '~/common/application-bootstrap';
import { invariant } from '~/common/utils/invariant';
import { safeToUseInsomniaFileNameWithExt } from '~/sync/git/insomnia-filename';
import { AnalyticsEvent } from '~/ui/analytics';
Expand All @@ -17,7 +17,7 @@ interface WorkspacePatch {
mockServerUrl?: string;
}

export async function clientAction({ request }: Route.ClientActionArgs) {
export async function clientAction({ request, context }: Route.ClientActionArgs) {
const patch = (await request.json()) as WorkspacePatch;
const workspaceId = patch.workspaceId;
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
Expand Down Expand Up @@ -61,7 +61,7 @@ export async function clientAction({ request }: Route.ClientActionArgs) {

patch.name = patch.name || workspace.name || (workspace.scope === 'collection' ? 'My Collection' : 'my-spec.yaml');

await renameWorkspace(workspace._id, patch.name);
await context.get(InsomniaContext).workspace.renameById(workspace._id, patch.name);

const project = await services.project.getById(workspace.parentId);
invariant(project, 'Project not found');
Expand Down
Loading