mirror of
https://github.com/supabase/supabase.git
synced 2026-05-11 02:20:29 +08:00
* refactor: reading markdown docs files
Refactor how Markdown docs files are read:
- Reuses the same logic across search index generation & page generation
- Improves the indexed content for search:
- Stops removing MDX components, which often contain useful
information like Admonitions
- Denormalizes Partials and CodeSamples for more complete content
This is a prerequisite step for implementing the "Copy docs as Markdown"
functionality.
Only touches regular guides for now, not federated ones.
* fix: tailwind build error (#37728)
We changed to default to ESM imports a while ago, which means local
builds are now breaking because the Tailwind uses a require. Changed to
CJS for Tailwind config file. (I have no idea how this has been working
on Vercel all this time.)
* style: prettier
242 lines
6.7 KiB
TypeScript
242 lines
6.7 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
import { Result } from '~/features/helpers.fn'
|
|
import { type OpenAIClientInterface } from '~/lib/openAi'
|
|
import { ApiError } from '../../utils'
|
|
import { POST } from '../route'
|
|
|
|
const contentEmbeddingMock = vi
|
|
.fn()
|
|
.mockImplementation(async () => Result.ok({ embedding: [0.1, 0.2, 0.3], tokenCount: 10 }))
|
|
const openAIMock: OpenAIClientInterface = {
|
|
createContentEmbedding: contentEmbeddingMock,
|
|
}
|
|
vi.mock(import('~/lib/openAi'), () => ({
|
|
openAI: () => openAIMock,
|
|
}))
|
|
|
|
const rpcSpy = vi.fn().mockImplementation((funcName, params) => {
|
|
if (funcName === 'search_content_hybrid') {
|
|
const limit = params?.max_result || 2
|
|
const mockResults = [
|
|
{
|
|
type: 'markdown',
|
|
page_title: 'Test Guide',
|
|
href: '/guides/test',
|
|
content: params?.include_full_content ? 'Test content' : null,
|
|
subsections: [
|
|
{ title: 'Introduction', content: 'Introduction content' },
|
|
{ title: 'Details', content: 'Details content' },
|
|
],
|
|
},
|
|
{
|
|
type: 'markdown',
|
|
page_title: 'Another Guide',
|
|
href: '/guides/another',
|
|
content: params?.include_full_content ? 'Another content' : null,
|
|
subsections: [{ title: 'Getting Started', content: 'Getting Started content' }],
|
|
},
|
|
{
|
|
type: 'reference',
|
|
page_title: 'Create a SSO provider',
|
|
href: 'https://supabase.com/docs/reference/api/v1-create-a-sso-provider',
|
|
content: params?.include_full_content ? 'Creates a new SSO provider for a project' : null,
|
|
metadata: {
|
|
title: 'Create a SSO provider',
|
|
subtitle: 'Management API Reference: Create a SSO provider',
|
|
},
|
|
},
|
|
]
|
|
return Promise.resolve({ data: mockResults.slice(0, limit), error: null })
|
|
}
|
|
return Promise.resolve({ data: [], error: null })
|
|
})
|
|
vi.mock('~/lib/supabase', () => ({
|
|
supabase: () => ({
|
|
rpc: rpcSpy,
|
|
}),
|
|
}))
|
|
|
|
describe('/api/graphql searchDocs', () => {
|
|
beforeAll(() => {
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
})
|
|
|
|
afterAll(() => {
|
|
vi.restoreAllMocks()
|
|
vi.doUnmock('~/lib/openAi')
|
|
vi.doUnmock('~/lib/supabase')
|
|
})
|
|
|
|
it('should return search results when given a valid query', async () => {
|
|
const searchQuery = `
|
|
query {
|
|
searchDocs(query: "authentication") {
|
|
nodes {
|
|
title
|
|
href
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: searchQuery }),
|
|
})
|
|
|
|
const response = await POST(request)
|
|
const json = await response.json()
|
|
|
|
expect(json.errors).toBeUndefined()
|
|
expect(json.data).toBeDefined()
|
|
expect(json.data.searchDocs).toBeDefined()
|
|
expect(json.data.searchDocs.nodes).toBeInstanceOf(Array)
|
|
expect(json.data.searchDocs.nodes).toHaveLength(3)
|
|
expect(json.data.searchDocs.nodes[0]).toMatchObject({
|
|
title: 'Test Guide',
|
|
href: '/guides/test',
|
|
})
|
|
})
|
|
|
|
it('should respect the limit parameter', async () => {
|
|
const searchQuery = `
|
|
query {
|
|
searchDocs(query: "database", limit: 1) {
|
|
nodes {
|
|
title
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: searchQuery }),
|
|
})
|
|
|
|
const response = await POST(request)
|
|
const json = await response.json()
|
|
|
|
expect(json.errors).toBeUndefined()
|
|
expect(json.data.searchDocs.nodes).toHaveLength(1)
|
|
expect(json.data.searchDocs.nodes[0].title).toBe('Test Guide')
|
|
expect(rpcSpy).toHaveBeenCalledWith(
|
|
'search_content_hybrid',
|
|
expect.objectContaining({
|
|
max_result: 1,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should include content field when requested', async () => {
|
|
const searchQuery = `
|
|
query {
|
|
searchDocs(query: "api") {
|
|
nodes {
|
|
title
|
|
content
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: searchQuery }),
|
|
})
|
|
|
|
const response = await POST(request)
|
|
const json = await response.json()
|
|
|
|
expect(json.errors).toBeUndefined()
|
|
expect(json.data.searchDocs.nodes[0].content).toBe('Test content')
|
|
expect(rpcSpy).toHaveBeenCalledWith(
|
|
'search_content_hybrid',
|
|
expect.objectContaining({
|
|
include_full_content: true,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should handle errors from embedding creation', async () => {
|
|
contentEmbeddingMock.mockImplementationOnce(() => {
|
|
return Result.error(new ApiError('Embedding generation failed'))
|
|
})
|
|
|
|
const searchQuery = `
|
|
query {
|
|
searchDocs(query: "failed query") {
|
|
nodes {
|
|
title
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: searchQuery }),
|
|
})
|
|
|
|
const response = await POST(request)
|
|
const json = await response.json()
|
|
|
|
expect(json.errors).toBeDefined()
|
|
expect(json.errors[0].message).toBe('Internal Server Error')
|
|
})
|
|
|
|
it('should require a query parameter', async () => {
|
|
const searchQuery = `
|
|
query {
|
|
searchDocs {
|
|
nodes {
|
|
title
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: searchQuery }),
|
|
})
|
|
|
|
const response = await POST(request)
|
|
const json = await response.json()
|
|
|
|
expect(json.errors).toBeDefined()
|
|
expect(json.errors[0].message).toContain('required')
|
|
})
|
|
|
|
it('should return Management API references with proper fields', async () => {
|
|
const searchQuery = `
|
|
query {
|
|
searchDocs(query: "SSO provider", limit: 3) {
|
|
nodes {
|
|
... on ManagementApiReference {
|
|
title
|
|
href
|
|
content
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: searchQuery }),
|
|
})
|
|
|
|
const response = await POST(request)
|
|
const json = await response.json()
|
|
|
|
expect(json.errors).toBeUndefined()
|
|
expect(json.data).toBeDefined()
|
|
expect(json.data.searchDocs).toBeDefined()
|
|
expect(json.data.searchDocs.nodes).toBeInstanceOf(Array)
|
|
expect(json.data.searchDocs.nodes).toHaveLength(3)
|
|
|
|
const managementApiNode = json.data.searchDocs.nodes[2]
|
|
expect(managementApiNode).toMatchObject({
|
|
title: 'Create a SSO provider',
|
|
href: 'https://supabase.com/docs/reference/api/v1-create-a-sso-provider',
|
|
content: 'Creates a new SSO provider for a project',
|
|
})
|
|
})
|
|
})
|