refactor: use existing useApi composable in prep store, remove duplicate
Delete useApiFetch.ts wrapper (returned T|null) and update prep.ts and
prep.test.ts to import useApiFetch from useApi.ts directly, destructuring
{ data, error } to match the established pattern used by all other stores.
This commit is contained in:
parent
44adfd6691
commit
dc21e730d9
3 changed files with 48 additions and 58 deletions
|
|
@ -1,10 +0,0 @@
|
||||||
/**
|
|
||||||
* Convenience wrapper around useApiFetch from useApi.ts that returns data directly
|
|
||||||
* (null on error), simplifying store code that doesn't need detailed error handling.
|
|
||||||
*/
|
|
||||||
import { useApiFetch as _useApiFetch } from './useApi'
|
|
||||||
|
|
||||||
export async function useApiFetch<T>(url: string, opts?: RequestInit): Promise<T | null> {
|
|
||||||
const { data } = await _useApiFetch<T>(url, opts)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
@ -3,11 +3,11 @@ import { setActivePinia, createPinia } from 'pinia'
|
||||||
import { usePrepStore } from './prep'
|
import { usePrepStore } from './prep'
|
||||||
|
|
||||||
// Mock useApiFetch
|
// Mock useApiFetch
|
||||||
vi.mock('../composables/useApiFetch', () => ({
|
vi.mock('../composables/useApi', () => ({
|
||||||
useApiFetch: vi.fn(),
|
useApiFetch: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { useApiFetch } from '../composables/useApiFetch'
|
import { useApiFetch } from '../composables/useApi'
|
||||||
|
|
||||||
describe('usePrepStore', () => {
|
describe('usePrepStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -23,14 +23,14 @@ describe('usePrepStore', () => {
|
||||||
it('fetchFor loads research, contacts, task, and full job in parallel', async () => {
|
it('fetchFor loads research, contacts, task, and full job in parallel', async () => {
|
||||||
const mockApiFetch = vi.mocked(useApiFetch)
|
const mockApiFetch = vi.mocked(useApiFetch)
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ company_brief: 'Acme', ceo_brief: null, talking_points: null,
|
.mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null,
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: '2026-03-20T12:00:00' }) // research
|
generated_at: '2026-03-20T12:00:00' }, error: null }) // research
|
||||||
.mockResolvedValueOnce([]) // contacts
|
.mockResolvedValueOnce({ data: [], error: null }) // contacts
|
||||||
.mockResolvedValueOnce({ status: 'none', stage: null, message: null }) // task
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task
|
||||||
.mockResolvedValueOnce({ id: 1, title: 'Engineer', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null,
|
||||||
description: 'Build things.', cover_letter: null, match_score: 80,
|
description: 'Build things.', cover_letter: null, match_score: 80,
|
||||||
keyword_gaps: null }) // fullJob
|
keyword_gaps: null }, error: null }) // fullJob
|
||||||
|
|
||||||
const store = usePrepStore()
|
const store = usePrepStore()
|
||||||
await store.fetchFor(1)
|
await store.fetchFor(1)
|
||||||
|
|
@ -46,13 +46,13 @@ describe('usePrepStore', () => {
|
||||||
const mockApiFetch = vi.mocked(useApiFetch)
|
const mockApiFetch = vi.mocked(useApiFetch)
|
||||||
// First call for job 1
|
// First call for job 1
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ company_brief: 'OldCo', ceo_brief: null, talking_points: null,
|
.mockResolvedValueOnce({ data: { company_brief: 'OldCo', ceo_brief: null, talking_points: null,
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: null })
|
generated_at: null }, error: null })
|
||||||
.mockResolvedValueOnce([])
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
.mockResolvedValueOnce({ status: 'none', stage: null, message: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ id: 1, title: 'Old Job', company: 'OldCo', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Old Job', company: 'OldCo', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
||||||
const store = usePrepStore()
|
const store = usePrepStore()
|
||||||
await store.fetchFor(1)
|
await store.fetchFor(1)
|
||||||
|
|
@ -60,11 +60,11 @@ describe('usePrepStore', () => {
|
||||||
|
|
||||||
// Second call for job 2 - clears first
|
// Second call for job 2 - clears first
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce(null) // 404 → null
|
.mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 404, detail: '' } }) // 404 → null
|
||||||
.mockResolvedValueOnce([])
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
.mockResolvedValueOnce({ status: 'none', stage: null, message: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ id: 2, title: 'New Job', company: 'NewCo', url: null,
|
.mockResolvedValueOnce({ data: { id: 2, title: 'New Job', company: 'NewCo', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
||||||
await store.fetchFor(2)
|
await store.fetchFor(2)
|
||||||
expect(store.research).toBeNull()
|
expect(store.research).toBeNull()
|
||||||
|
|
@ -73,14 +73,14 @@ describe('usePrepStore', () => {
|
||||||
|
|
||||||
it('generateResearch calls POST then starts polling', async () => {
|
it('generateResearch calls POST then starts polling', async () => {
|
||||||
const mockApiFetch = vi.mocked(useApiFetch)
|
const mockApiFetch = vi.mocked(useApiFetch)
|
||||||
mockApiFetch.mockResolvedValueOnce({ task_id: 7, is_new: true })
|
mockApiFetch.mockResolvedValueOnce({ data: { task_id: 7, is_new: true }, error: null })
|
||||||
|
|
||||||
const store = usePrepStore()
|
const store = usePrepStore()
|
||||||
store.currentJobId = 1
|
store.currentJobId = 1
|
||||||
|
|
||||||
// Spy on pollTask via the interval
|
// Spy on pollTask via the interval
|
||||||
const pollSpy = mockApiFetch
|
const pollSpy = mockApiFetch
|
||||||
.mockResolvedValueOnce({ status: 'running', stage: 'Analyzing', message: null })
|
.mockResolvedValueOnce({ data: { status: 'running', stage: 'Analyzing', message: null }, error: null })
|
||||||
|
|
||||||
await store.generateResearch(1)
|
await store.generateResearch(1)
|
||||||
|
|
||||||
|
|
@ -98,28 +98,28 @@ describe('usePrepStore', () => {
|
||||||
const mockApiFetch = vi.mocked(useApiFetch)
|
const mockApiFetch = vi.mocked(useApiFetch)
|
||||||
// Set up store with a job loaded
|
// Set up store with a job loaded
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ company_brief: 'Acme', ceo_brief: null, talking_points: null,
|
.mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null,
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: null })
|
generated_at: null }, error: null })
|
||||||
.mockResolvedValueOnce([])
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
.mockResolvedValueOnce({ status: 'none', stage: null, message: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ id: 1, title: 'Eng', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
||||||
const store = usePrepStore()
|
const store = usePrepStore()
|
||||||
await store.fetchFor(1)
|
await store.fetchFor(1)
|
||||||
|
|
||||||
// Mock first poll → completed
|
// Mock first poll → completed
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ status: 'completed', stage: null, message: null })
|
.mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null })
|
||||||
// re-fetch on completed: research, contacts, task, fullJob
|
// re-fetch on completed: research, contacts, task, fullJob
|
||||||
.mockResolvedValueOnce({ company_brief: 'Updated!', ceo_brief: null, talking_points: null,
|
.mockResolvedValueOnce({ data: { company_brief: 'Updated!', ceo_brief: null, talking_points: null,
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: '2026-03-20T13:00:00' })
|
generated_at: '2026-03-20T13:00:00' }, error: null })
|
||||||
.mockResolvedValueOnce([])
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
.mockResolvedValueOnce({ status: 'completed', stage: null, message: null })
|
.mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ id: 1, title: 'Eng', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
||||||
description: 'Now with content', cover_letter: null, match_score: null, keyword_gaps: null })
|
description: 'Now with content', cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
||||||
store.pollTask(1)
|
store.pollTask(1)
|
||||||
await vi.advanceTimersByTimeAsync(3000)
|
await vi.advanceTimersByTimeAsync(3000)
|
||||||
|
|
@ -131,13 +131,13 @@ describe('usePrepStore', () => {
|
||||||
it('clear cancels polling interval and resets state', async () => {
|
it('clear cancels polling interval and resets state', async () => {
|
||||||
const mockApiFetch = vi.mocked(useApiFetch)
|
const mockApiFetch = vi.mocked(useApiFetch)
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ company_brief: 'Acme', ceo_brief: null, talking_points: null,
|
.mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null,
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: null })
|
generated_at: null }, error: null })
|
||||||
.mockResolvedValueOnce([])
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
.mockResolvedValueOnce({ status: 'none', stage: null, message: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ id: 1, title: 'Eng', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
||||||
const store = usePrepStore()
|
const store = usePrepStore()
|
||||||
await store.fetchFor(1)
|
await store.fetchFor(1)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useApiFetch } from '../composables/useApiFetch'
|
import { useApiFetch } from '../composables/useApi'
|
||||||
|
|
||||||
export interface ResearchBrief {
|
export interface ResearchBrief {
|
||||||
company_brief: string | null
|
company_brief: string | null
|
||||||
|
|
@ -70,17 +70,17 @@ export const usePrepStore = defineStore('prep', () => {
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [researchData, contactsData, taskData, jobData] = await Promise.all([
|
const [researchResult, contactsResult, taskResult, jobResult] = await Promise.all([
|
||||||
useApiFetch<ResearchBrief>(`/api/jobs/${jobId}/research`),
|
useApiFetch<ResearchBrief>(`/api/jobs/${jobId}/research`),
|
||||||
useApiFetch<Contact[]>(`/api/jobs/${jobId}/contacts`),
|
useApiFetch<Contact[]>(`/api/jobs/${jobId}/contacts`),
|
||||||
useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`),
|
useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`),
|
||||||
useApiFetch<FullJobDetail>(`/api/jobs/${jobId}`),
|
useApiFetch<FullJobDetail>(`/api/jobs/${jobId}`),
|
||||||
])
|
])
|
||||||
|
|
||||||
research.value = researchData ?? null
|
research.value = researchResult.data ?? null
|
||||||
contacts.value = (contactsData as Contact[]) ?? []
|
contacts.value = (contactsResult.data as Contact[]) ?? []
|
||||||
taskStatus.value = (taskData as TaskStatus) ?? { status: null, stage: null, message: null }
|
taskStatus.value = (taskResult.data as TaskStatus) ?? { status: null, stage: null, message: null }
|
||||||
fullJob.value = (jobData as FullJobDetail) ?? null
|
fullJob.value = (jobResult.data as FullJobDetail) ?? null
|
||||||
|
|
||||||
// If a task is already running/queued, start polling
|
// If a task is already running/queued, start polling
|
||||||
const ts = taskStatus.value.status
|
const ts = taskStatus.value.status
|
||||||
|
|
@ -95,14 +95,14 @@ export const usePrepStore = defineStore('prep', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateResearch(jobId: number) {
|
async function generateResearch(jobId: number) {
|
||||||
await useApiFetch(`/api/jobs/${jobId}/research/generate`, { method: 'POST' })
|
await useApiFetch<unknown>(`/api/jobs/${jobId}/research/generate`, { method: 'POST' })
|
||||||
pollTask(jobId)
|
pollTask(jobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollTask(jobId: number) {
|
function pollTask(jobId: number) {
|
||||||
_clearInterval()
|
_clearInterval()
|
||||||
pollInterval = setInterval(async () => {
|
pollInterval = setInterval(async () => {
|
||||||
const data = await useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`)
|
const { data } = await useApiFetch<TaskStatus>(`/api/jobs/${jobId}/research/task`)
|
||||||
if (data) {
|
if (data) {
|
||||||
taskStatus.value = data as TaskStatus
|
taskStatus.value = data as TaskStatus
|
||||||
if (data.status === 'completed' || data.status === 'failed') {
|
if (data.status === 'completed' || data.status === 'failed') {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue