feat: add mobile vault storage boundary
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createFixtureMobileVaultRepository } from './mobileVaultRepository'
|
||||
import { createMobileVaultConfig } from './mobileVaultConfig'
|
||||
import { createFixtureMobileVaultRepository, createStoredMobileVaultRepository } from './mobileVaultRepository'
|
||||
import { createMemoryMobileVaultStorage } from './mobileVaultStorage'
|
||||
import type { MobileNoteSource } from './mobileNoteProjection'
|
||||
|
||||
const sources: MobileNoteSource[] = [
|
||||
@@ -30,6 +32,24 @@ describe('mobile vault repository', () => {
|
||||
|
||||
await expect(repository.readNote('missing')).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('projects notes from app-local markdown storage', async () => {
|
||||
const repository = createStoredMobileVaultRepository({
|
||||
storage: createMemoryMobileVaultStorage([
|
||||
{ path: 'inbox/workflow.md', content: '# Workflow\n\nStored markdown body.' },
|
||||
{ path: 'release.md', content: '# Release\n\nRelease body.' },
|
||||
]),
|
||||
vault: createVault(),
|
||||
})
|
||||
|
||||
const notes = await repository.listNotes()
|
||||
|
||||
expect(notes.map((note) => note.id)).toEqual(['inbox/workflow', 'release'])
|
||||
await expect(repository.readNote('release')).resolves.toMatchObject({
|
||||
id: 'release',
|
||||
title: 'Release',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function createSource(id: string, title: string): MobileNoteSource {
|
||||
@@ -44,3 +64,12 @@ function createSource(id: string, title: string): MobileNoteSource {
|
||||
content: `---\ntitle: ${title}\n---\n\n# ${title}\n\nBody text for ${title}.`,
|
||||
}
|
||||
}
|
||||
|
||||
function createVault() {
|
||||
const result = createMobileVaultConfig({ id: 'personal', name: 'Personal Journal' })
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
return result.config
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { projectMobileNotes, type MobileNote, type MobileNoteSource } from './mobileNoteProjection'
|
||||
import { projectMobileNote, projectMobileNotes, type MobileNote, type MobileNoteSource } from './mobileNoteProjection'
|
||||
import type { MobileVaultConfig } from './mobileVaultConfig'
|
||||
import type { MobileVaultFile, MobileVaultStorageDriver } from './mobileVaultStorage'
|
||||
|
||||
export type MobileVaultRepository = {
|
||||
listNotes: () => Promise<MobileNote[]>
|
||||
@@ -13,3 +15,48 @@ export function createFixtureMobileVaultRepository(sources: MobileNoteSource[]):
|
||||
readNote: (id) => Promise.resolve(notes.find((note) => note.id === id) ?? null),
|
||||
}
|
||||
}
|
||||
|
||||
export function createStoredMobileVaultRepository({
|
||||
storage,
|
||||
vault,
|
||||
}: {
|
||||
storage: MobileVaultStorageDriver
|
||||
vault: MobileVaultConfig
|
||||
}): MobileVaultRepository {
|
||||
return {
|
||||
listNotes: async () => projectMobileNotes((await storage.listMarkdownFiles(vault)).map(fileToSource)),
|
||||
readNote: async (id) => {
|
||||
const file = await findFileById({ id, storage, vault })
|
||||
return file ? projectMobileNote(fileToSource(file)) : null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function findFileById({
|
||||
id,
|
||||
storage,
|
||||
vault,
|
||||
}: {
|
||||
id: string
|
||||
storage: MobileVaultStorageDriver
|
||||
vault: MobileVaultConfig
|
||||
}) {
|
||||
return (await storage.listMarkdownFiles(vault)).find((file) => fileId(file.path) === id) ?? null
|
||||
}
|
||||
|
||||
function fileToSource(file: MobileVaultFile): MobileNoteSource {
|
||||
return {
|
||||
id: fileId(file.path),
|
||||
type: 'Note',
|
||||
icon: 'file-text',
|
||||
date: '',
|
||||
modified: '',
|
||||
filename: file.path,
|
||||
content: file.content,
|
||||
tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
function fileId(path: string) {
|
||||
return path.replace(/\.md$/, '')
|
||||
}
|
||||
|
||||
38
apps/mobile/src/mobileVaultStorage.test.ts
Normal file
38
apps/mobile/src/mobileVaultStorage.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createMobileVaultConfig } from './mobileVaultConfig'
|
||||
import { createMemoryMobileVaultStorage } from './mobileVaultStorage'
|
||||
|
||||
const vault = createVault()
|
||||
|
||||
describe('mobile vault storage', () => {
|
||||
it('lists only markdown files in stable path order', async () => {
|
||||
const storage = createMemoryMobileVaultStorage([
|
||||
{ path: 'zeta.md', content: '# Zeta' },
|
||||
{ path: 'asset.png', content: 'binary' },
|
||||
{ path: 'alpha.md', content: '# Alpha' },
|
||||
])
|
||||
|
||||
await expect(storage.listMarkdownFiles(vault)).resolves.toEqual([
|
||||
{ path: 'alpha.md', content: '# Alpha' },
|
||||
{ path: 'zeta.md', content: '# Zeta' },
|
||||
])
|
||||
})
|
||||
|
||||
it('reads and writes markdown file content by vault path', async () => {
|
||||
const storage = createMemoryMobileVaultStorage([{ path: 'inbox.md', content: '# Inbox' }])
|
||||
|
||||
await storage.writeMarkdownFile(vault, 'inbox.md', '# Updated Inbox')
|
||||
|
||||
await expect(storage.readMarkdownFile(vault, 'inbox.md')).resolves.toBe('# Updated Inbox')
|
||||
await expect(storage.readMarkdownFile(vault, 'missing.md')).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
function createVault() {
|
||||
const result = createMobileVaultConfig({ id: 'personal', name: 'Personal Journal' })
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
return result.config
|
||||
}
|
||||
31
apps/mobile/src/mobileVaultStorage.ts
Normal file
31
apps/mobile/src/mobileVaultStorage.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { MobileVaultConfig } from './mobileVaultConfig'
|
||||
|
||||
export type MobileVaultFile = {
|
||||
path: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type MobileVaultStorageDriver = {
|
||||
listMarkdownFiles: (vault: MobileVaultConfig) => Promise<MobileVaultFile[]>
|
||||
readMarkdownFile: (vault: MobileVaultConfig, path: string) => Promise<string | null>
|
||||
writeMarkdownFile: (vault: MobileVaultConfig, path: string, content: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function createMemoryMobileVaultStorage(files: MobileVaultFile[]): MobileVaultStorageDriver {
|
||||
const fileByPath = new Map(files.map((file) => [file.path, file.content]))
|
||||
|
||||
return {
|
||||
listMarkdownFiles: () => Promise.resolve(markdownFiles(fileByPath)),
|
||||
readMarkdownFile: (_vault, path) => Promise.resolve(fileByPath.get(path) ?? null),
|
||||
writeMarkdownFile: async (_vault, path, content) => {
|
||||
fileByPath.set(path, content)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function markdownFiles(fileByPath: Map<string, string>) {
|
||||
return [...fileByPath.entries()]
|
||||
.filter(([path]) => path.endsWith('.md'))
|
||||
.map(([path, content]) => ({ path, content }))
|
||||
.sort((left, right) => left.path.localeCompare(right.path))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
|
||||
- Branch: `codex/mobile`
|
||||
- Active phase: Phase 2 - Mobile Shell
|
||||
- Active slice: Add supported TenTap Markdown serializer
|
||||
- Active slice: Add mobile vault storage driver boundary
|
||||
- Push policy: commit locally; do not push unless explicitly requested
|
||||
- Validation target: iPad/iOS simulator first
|
||||
|
||||
@@ -50,6 +50,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Created [ADR-0110](./adr/0110-tentap-mobile-editor-spike.md) for the TenTap mobile editor spike and its acceptance gates.
|
||||
- Added a TenTap draft callback boundary that captures editor HTML but explicitly marks it non-persistable until Markdown serialization exists, preventing HTML from becoming canonical vault content by accident.
|
||||
- Added the first supported TenTap HTML-to-Markdown serializer for H1, paragraph, and unordered-list output; unsupported HTML remains blocked from persistence.
|
||||
- Added a mobile vault storage driver contract plus memory implementation, and connected the mobile vault repository to app-local markdown files behind that storage interface.
|
||||
|
||||
## Next Action
|
||||
|
||||
@@ -57,7 +58,7 @@ Continue Phase 2 with the next mobile shell slice:
|
||||
|
||||
1. Dismiss or suppress Expo Go's first-run tools modal during simulator QA so screenshots capture the app without the overlay.
|
||||
2. Expand the serializer for additional TenTap output needed by real notes: links, emphasis, code, headings, ordered lists, and task lists.
|
||||
3. Add the first native storage adapter around the vault config/repository contracts after the editor spike confirms the app shape.
|
||||
3. Implement the Expo FileSystem-backed storage driver behind the new mobile vault storage contract.
|
||||
|
||||
## Verification Log
|
||||
|
||||
@@ -148,6 +149,10 @@ Continue Phase 2 with the next mobile shell slice:
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after supported Markdown serialization.
|
||||
- CodeScene after supported Markdown serialization: `apps/mobile/src/mobileEditorDraft.ts`, `apps/mobile/src/mobileEditorDraft.test.ts`, `apps/mobile/src/mobileEditorDocument.ts`, and `apps/mobile/src/MobileEditorAdapter.tsx` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after supported Markdown serialization.
|
||||
- `pnpm --filter @tolaria/mobile test -- src/mobileVaultStorage.test.ts src/mobileVaultRepository.test.ts` passed after storage boundary extraction: 10 files / 35 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after storage boundary extraction.
|
||||
- CodeScene after storage boundary extraction: `apps/mobile/src/mobileVaultStorage.ts`, `apps/mobile/src/mobileVaultStorage.test.ts`, `apps/mobile/src/mobileVaultRepository.ts`, and `apps/mobile/src/mobileVaultRepository.test.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after storage boundary extraction.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user