Skip to content
Merged
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
18 changes: 4 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,11 @@ If you change the UI API client or routes, regenerate the checked-in outputs wit
pnpm codegen:ui
```

## Run in mock mode
## Run with the mock API

This is the fastest way to explore the UI. It runs Vite with in-browser mock
services, so no backend process is required.

```bash
VITE_SERVICE_MODE=mock pnpm dev:ui
```

Open `http://localhost:5173`.

## Web mode with mock API

Use this mode to exercise the generated API client against the local mock API
server. Run both commands from the repository root in separate terminals.
Use this mode for local UI development against the generated API client and
mock API server. Run both commands from the repository root in separate
terminals.

Terminal 1:

Expand Down
5 changes: 5 additions & 0 deletions api/mock-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
"./browser": "./src/browser.ts",
"./package.json": "./package.json"
},
"scripts": {
"build": "node scripts/build.mjs",
"check": "tsc -p tsconfig.json --noEmit",
Expand All @@ -14,6 +18,7 @@
},
"dependencies": {
"@hono/node-server": "^1.19.7",
"@msw/data": "1.1.6",
"hono": "^4.12.23",
"zod": "^4.4.3"
},
Expand Down
8 changes: 4 additions & 4 deletions api/mock-server/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const json = async <T>(response: Response) => response.json() as Promise<T>

describe('mock api app', () => {
it('creates counter ids and exposes them through the workspace list', async () => {
const app = newMockApiApp()
const app = await newMockApiApp()

const createResponse = await app.fetch(
new Request('http://localhost/api/v1/workspaces', {
Expand Down Expand Up @@ -38,7 +38,7 @@ describe('mock api app', () => {
})

it('mounts the product routes under a custom base path', async () => {
const app = newMockApiApp({ basePath: '/custom' })
const app = await newMockApiApp({ basePath: '/custom' })

const response = await app.fetch(new Request('http://localhost/custom/workspaces'))
expect(response.status).toBe(200)
Expand All @@ -50,7 +50,7 @@ describe('mock api app', () => {
})

it('keeps seeded deck stats aligned with seeded notes', async () => {
const app = newMockApiApp()
const app = await newMockApiApp()

const response = await app.fetch(new Request('http://localhost/__mock/state'))

Expand All @@ -70,7 +70,7 @@ describe('mock api app', () => {
})

it('exposes admin state reset and inspection endpoints', async () => {
const app = newMockApiApp()
const app = await newMockApiApp()

const createResponse = await app.fetch(
new Request('http://localhost/api/v1/workspaces', {
Expand Down
9 changes: 5 additions & 4 deletions api/mock-server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@ export type NewMockApiAppOptions = {
controllers?: MockApiControllers
}

export const newMockApiApp = ({
export const newMockApiApp = async ({
basePath = "/api/v1",
controllers = newMemoryMockApiControllers(),
controllers,
}: NewMockApiAppOptions = {}) => {
const app = new Hono()
const mockControllers = controllers ?? await newMemoryMockApiControllers()

app.use('*', cors())
registerGeneratedMockRoutes(app, {
controllers,
controllers: mockControllers,
runtime: adminRuntime,
})
registerGeneratedMockRoutes(app, {
basePath: basePath,
controllers,
controllers: mockControllers,
runtime: clearWebApiRuntime,
})

Expand Down
31 changes: 31 additions & 0 deletions api/mock-server/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export { newMockApiDependencies, type MockApiDependencies } from './dependencies.ts'
export {
newBrowserMockStateStore,
type BrowserMockStateStoreOptions,
} from './lib/browserStateStore.ts'
export { DEFAULT_SETTINGS } from './features/settings/defaults.ts'
export { seedState } from './generated/mock-admin/state/seed.ts'
export { zMockState } from './generated/mock-admin/contract/zod.gen.ts'
export type { MockState, SettingsRecord } from './generated/mock-admin/contract/index.ts'
export type {
Deck,
DeckDraft,
Folder,
FolderDraft,
NoteDetail,
NoteDraft,
NoteListItem,
NoteRef,
ReviewGrade,
ReviewSession,
ReviewStartResult,
SearchRequest,
SearchResultGroup,
Settings,
TrashState,
Workspace,
WorkspaceDraft,
WorkspaceListResult,
} from './generated/clear-web-api/contract/types.gen.ts'
export { MockHttpError } from './generated/clear-web-api/mock-runtime.ts'
export type { MockStateStore } from './lib/stateStore.ts'
150 changes: 34 additions & 116 deletions api/mock-server/src/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,17 @@ import {
type GeneratedMockControllers as ClearWebApiGeneratedMockControllers,
} from './generated/clear-web-api/mock-runtime.ts'
import type { GeneratedMockControllers as AdminGeneratedMockControllers } from './generated/mock-admin/mock-runtime.ts'
import { newAdminStateController } from './generated/mock-admin/state/controller.ts'
import { MockStateRepository, type MockStateOptions } from './generated/mock-admin/state/repository.ts'
import { AdminStateService } from './generated/mock-admin/state/service.ts'
import { seedState } from './generated/mock-admin/state/seed.ts'
import type { MockState } from './generated/mock-admin/contract/index.ts'
import { WorkspaceRepository } from './features/workspaces/repository.ts'
import { FolderRepository } from './features/folders/repository.ts'
import { DeckRepository } from './features/decks/repository.ts'
import { NotesRepository } from './features/notes/repository.ts'
import { ReviewRepository } from './features/review/repository.ts'
import { SettingsRepository } from './features/settings/repository.ts'
import { TrashRepository } from './features/trash/repository.ts'
import { LocationPathResolver } from './features/location-path/resolver.ts'
import { WorkspacesService } from './features/workspaces/service.ts'
import { FolderService } from './features/folders/service.ts'
import { DeckService } from './features/decks/service.ts'
import { NotesService } from './features/notes/service.ts'
import { ReviewService } from './features/review/service.ts'
import { SettingsService } from './features/settings/service.ts'
import { TrashService } from './features/trash/service.ts'
import { SearchService } from './features/search/service.ts'
import type { MockClock, MockState } from './generated/mock-admin/contract/index.ts'
import {
newMockApiDependencies,
type MockApiDependencies,
} from './dependencies.ts'
import {
newFileMockStateStore,
type MockStateOptions,
} from './lib/nodeStateStore.ts'
import type { MockStateStore } from './lib/stateStore.ts'
import { newBootstrapController } from './features/bootstrap/controllers/bootstrap.ts'
import { newListWorkspacesController } from './features/workspaces/controllers/listWorkspaces.ts'
import { newCreateWorkspaceController } from './features/workspaces/controllers/createWorkspace.ts'
Expand Down Expand Up @@ -68,101 +58,13 @@ import { newSearchContentController } from './features/search/controllers/search

export type ProductMockControllers = ClearWebApiGeneratedMockControllers
export type MockApiControllers = ProductMockControllers & AdminGeneratedMockControllers
export type MockApiDependencies = {
stateRepository: MockStateRepository
workspacesService: WorkspacesService
foldersService: FolderService
decksService: DeckService
notesService: NotesService
reviewService: ReviewService
settingsService: SettingsService
trashService: TrashService
searchService: SearchService
}
export type { MockApiDependencies }

export const newMockApiControllers = (
export const newMockApiControllers = async (
options: MockStateOptions = {},
): MockApiControllers => {
const stateRepository = new MockStateRepository(options)
const workspacesRepository = new WorkspaceRepository(stateRepository)
const foldersRepository = new FolderRepository(stateRepository)
const decksRepository = new DeckRepository(stateRepository)
const notesRepository = new NotesRepository(stateRepository)
const reviewRepository = new ReviewRepository(stateRepository)
const settingsRepository = new SettingsRepository(stateRepository)
const trashRepository = new TrashRepository(stateRepository)
const paths = new LocationPathResolver(workspacesRepository, foldersRepository, decksRepository)
const settingsService = new SettingsService(settingsRepository, stateRepository)
const notesService = new NotesService(
notesRepository,
decksRepository,
workspacesRepository,
trashRepository,
paths,
stateRepository,
)
const workspacesService = new WorkspacesService(
workspacesRepository,
foldersRepository,
decksRepository,
notesRepository,
trashRepository,
paths,
stateRepository,
)
const foldersService = new FolderService(
foldersRepository,
workspacesRepository,
decksRepository,
notesRepository,
trashRepository,
paths,
stateRepository,
)
const decksService = new DeckService(
decksRepository,
workspacesRepository,
foldersRepository,
notesRepository,
trashRepository,
paths,
stateRepository,
)
const reviewService = new ReviewService(
reviewRepository,
notesRepository,
decksRepository,
notesService,
stateRepository,
)
const trashService = new TrashService(
trashRepository,
workspacesRepository,
foldersRepository,
decksRepository,
notesRepository,
paths,
stateRepository,
)
const searchService = new SearchService(
workspacesRepository,
foldersRepository,
decksRepository,
notesRepository,
paths,
)
const deps: MockApiDependencies = {
stateRepository,
workspacesService,
foldersService,
decksService,
notesService,
reviewService,
settingsService,
trashService,
searchService,
}
const adminStateService = new AdminStateService(stateRepository, clearWebApiGeneratedRouteDefinitions.length)
): Promise<MockApiControllers> => {
const stateStore = await newFileMockStateStore(options)
const deps = newMockApiDependencies(stateStore)

return {
...newBootstrapController(deps),
Expand Down Expand Up @@ -203,10 +105,26 @@ export const newMockApiControllers = (
...newRestoreTrashItemController(deps),
...newDeleteTrashItemController(deps),
...newSearchContentController(deps),
...newAdminStateController(adminStateService),
...newAdminStateControllers(stateStore, clearWebApiGeneratedRouteDefinitions.length),
} as MockApiControllers
}

export const newMemoryMockApiControllers = (
export const newMemoryMockApiControllers = async (
initialState: MockState = seedState(),
): MockApiControllers => newMockApiControllers({ initialState })
): Promise<MockApiControllers> => newMockApiControllers({ initialState })

const newAdminStateControllers = (
stateStore: MockStateStore,
operationCount: number,
): AdminGeneratedMockControllers => ({
mockGetSnapshot: () => stateStore.snapshot(),
mockGetState: () => stateStore.snapshot(),
mockHealth: () => ({
ok: true,
operationCount,
}),
mockPostSnapshot: ({ body }) => stateStore.replace(body as MockState),
mockPutClock: ({ body }) => stateStore.setClock((body as MockClock).now),
mockPutState: ({ body }) => stateStore.replace(body as MockState),
mockResetState: () => stateStore.reset(),
})
Loading