feat(content api): add client library api reference search results (#35484)

* feat(content api): add client library api reference search results

Allow searchDocs results to also return function references from the
client library APIs

* fix(content api): refine language enum handling
This commit is contained in:
Charis authored and GitHub committed 2025-05-06 13:11:29 -04:00
1 parent 22ee5dfe2a
commit badcf17f70
11 files changed
+249 -29

No files matched your search

@@ -70,6 +70,35 @@ type Subsection {
content: String
}
"""
A reference document containing a description of a function from a Supabase client library
"""
type ClientLibraryFunctionReference implements SearchResult {
"""The title of the document"""
title: String
"""The URL of the document"""
href: String
"""The content of the reference document, as text"""
content: String
"""The programming language for which the function is written"""
language: Language!
"""The name of the function or method"""
methodName: String
}
enum Language {
JAVASCRIPT
SWIFT
DART
CSHARP
KOTLIN
PYTHON
}
type RootQueryType {
"""Get the GraphQL schema for this endpoint"""
schema: String!
+7 -2
View File
@@ -15,8 +15,11 @@ type Database = {
Returns: Array<
Omit<
DatabaseGenerated['public']['Functions']['search_content']['Returns'][number],
'subsections'
> & { subsections: Array<{ title?: string; href?: string; content?: string }> }
'subsections' | 'metadata'
> & {
metadata: { language?: string; methodName?: string }
subsections: Array<{ title?: string; href?: string; content?: string }>
}
>
}
}
@@ -38,3 +41,5 @@ export function supabase() {
return _supabase
}
export type { Database as DatabaseCorrected }
@@ -2,8 +2,10 @@ import { type RootQueryTypeSearchDocsArgs } from '~/__generated__/graphql'
import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
import { Result } from '~/features/helpers.fn'
import { openAI } from '~/lib/openAi'
import { supabase } from '~/lib/supabase'
import { supabase, type DatabaseCorrected } from '~/lib/supabase'
import { GuideModel } from '../guide/guideModel'
import { ReferenceSDKFunctionModel, SDKLanguageValues } from '../reference/referenceSDKModel'
import { SearchResultInterface } from './globalSearchInterface'
export abstract class SearchResultModel {
static async search(
@@ -22,26 +24,42 @@ export abstract class SearchResultModel {
max_result: args.limit,
})
)
.map((matches) =>
matches
.map(({ type, page_title, href, content, subsections }) => {
switch (type) {
case 'markdown':
return new GuideModel({
title: page_title,
href,
content,
subsections,
})
default:
return null
}
})
.filter(Boolean)
)
.map((matches) => matches.map(createModelFromMatch).filter(Boolean))
.mapError(convertPostgrestToApiError)
return matchResult
})
}
}
function createModelFromMatch({
type,
page_title,
href,
content,
metadata,
subsections,
}: DatabaseCorrected['public']['Functions']['search_content']['Returns'][number]): SearchResultInterface | null {
switch (type) {
case 'markdown':
return new GuideModel({
title: page_title,
href,
content,
subsections,
})
case 'reference':
const { language } = metadata
if (SDKLanguageValues.includes(language)) {
return new ReferenceSDKFunctionModel({
title: page_title,
href,
content,
language,
methodName: metadata.methodName,
})
}
default:
return null
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { SearchResultInterface } from '../globalSearch/globalSearchInterface'
import { type SearchResultInterface } from '../globalSearch/globalSearchInterface'
export class GuideModel implements SearchResultInterface {
public title?: string
@@ -0,0 +1,61 @@
import { type SearchResultInterface } from '../globalSearch/globalSearchInterface'
export const SDKLanguages: Record<string, { value: string; pathSection: string }> = {
JAVASCRIPT: {
value: 'JavaScript',
pathSection: 'javascript',
},
SWIFT: {
value: 'Swift',
pathSection: 'swift',
},
DART: {
value: 'Dart',
pathSection: 'dart',
},
CSHARP: {
value: 'C#',
pathSection: 'csharp',
},
KOTLIN: {
value: 'Kotlin',
pathSection: 'kotlin',
},
PYTHON: {
value: 'Python',
pathSection: 'python',
},
}
export const SDKLanguageValues = Object.values(SDKLanguages).map(({ value }) => value)
export class ReferenceSDKFunctionModel implements SearchResultInterface {
public title?: string
public href?: string
public content?: string
public language: string
public methodName?: string
constructor({
title,
href,
content,
language,
methodName,
}: {
title?: string
href?: string
content?: string
language: string
methodName?: string
}) {
this.title = title
this.href = href
this.content = content
this.methodName = methodName
if (SDKLanguageValues.includes(language)) {
this.language = language
}
}
}
@@ -0,0 +1,41 @@
import { GraphQLEnumType, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'
import { GraphQLInterfaceTypeSearchResult } from '../globalSearch/globalSearchSchema'
import { ReferenceSDKFunctionModel, SDKLanguages } from './referenceSDKModel'
const GraphQLEnumLanguage = new GraphQLEnumType({
name: 'Language',
values: Object.keys(SDKLanguages).reduce((acc, key) => {
acc[key] = { value: SDKLanguages[key].value }
return acc
}, {}),
})
export const GraphQLObjectTypeReferenceSDKFunction = new GraphQLObjectType({
name: 'ClientLibraryFunctionReference',
interfaces: [GraphQLInterfaceTypeSearchResult],
isTypeOf: (value: unknown) => value instanceof ReferenceSDKFunctionModel,
description:
'A reference document containing a description of a function from a Supabase client library',
fields: {
title: {
type: GraphQLString,
description: 'The title of the document',
},
href: {
type: GraphQLString,
description: 'The URL of the document',
},
content: {
type: GraphQLString,
description: 'The content of the reference document, as text',
},
language: {
type: new GraphQLNonNull(GraphQLEnumLanguage),
description: 'The programming language for which the function is written',
},
methodName: {
type: GraphQLString,
description: 'The name of the function or method',
},
},
})
+2 -1
View File
@@ -8,6 +8,7 @@ import {
import { RootQueryTypeResolvers } from '~/__generated__/graphql'
import { searchRoot } from './globalSearch/globalSearchResolver'
import { GraphQLObjectTypeGuide } from './guide/guideSchema'
import { GraphQLObjectTypeReferenceSDKFunction } from './reference/referenceSDKSchema'
const GRAPHQL_FIELD_INTROSPECT = 'schema' as const
@@ -34,5 +35,5 @@ export const rootGraphQLSchema = new GraphQLSchema({
...searchRoot,
},
}),
types: [GraphQLObjectTypeGuide],
types: [GraphQLObjectTypeGuide, GraphQLObjectTypeReferenceSDKFunction],
})
+6 -6
View File
@@ -56,7 +56,7 @@ export async function fetchJsLibReferenceSource() {
return new ClientLibReferenceLoader(
'js-lib',
'/reference/javascript',
{ title: 'JavaScript Reference' },
{ title: 'JavaScript Reference', language: 'JavaScript' },
'spec/supabase_js_v2.yml',
'spec/common-client-libs-sections.json'
).load()
@@ -66,7 +66,7 @@ export async function fetchDartLibReferenceSource() {
return new ClientLibReferenceLoader(
'dart-lib',
'/reference/dart',
{ title: 'Dart Reference' },
{ title: 'Dart Reference', language: 'Dart' },
'spec/supabase_dart_v2.yml',
'spec/common-client-libs-sections.json'
).load()
@@ -76,7 +76,7 @@ export async function fetchPythonLibReferenceSource() {
return new ClientLibReferenceLoader(
'python-lib',
'/reference/python',
{ title: 'Python Reference' },
{ title: 'Python Reference', language: 'Python' },
'spec/supabase_py_v2.yml',
'spec/common-client-libs-sections.json'
).load()
@@ -86,7 +86,7 @@ export async function fetchCSharpLibReferenceSource() {
return new ClientLibReferenceLoader(
'csharp-lib',
'/reference/csharp',
{ title: 'C# Reference' },
{ title: 'C# Reference', language: 'C#' },
'spec/supabase_csharp_v0.yml',
'spec/common-client-libs-sections.json'
).load()
@@ -96,7 +96,7 @@ export async function fetchSwiftLibReferenceSource() {
return new ClientLibReferenceLoader(
'swift-lib',
'/reference/swift',
{ title: 'Swift Reference' },
{ title: 'Swift Reference', language: 'Swift' },
'spec/supabase_swift_v2.yml',
'spec/common-client-libs-sections.json'
).load()
@@ -106,7 +106,7 @@ export async function fetchKtLibReferenceSource() {
return new ClientLibReferenceLoader(
'kt-lib',
'/reference/kotlin',
{ title: 'Kotlin Reference' },
{ title: 'Kotlin Reference', language: 'Kotlin' },
'spec/supabase_kt_v1.yml',
'spec/common-client-libs-sections.json'
).load()
@@ -52,7 +52,7 @@ export abstract class ReferenceLoader<SpecSection> extends BaseLoader {
`${this.path}/${refSection.slug}`,
refSection,
specSection,
this.meta
this.enhanceMeta(specSection)
)
})
.filter(Boolean)
@@ -62,6 +62,9 @@ export abstract class ReferenceLoader<SpecSection> extends BaseLoader {
abstract getSpecSections(specContents: string): SpecSection[]
abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection
enhanceMeta(section: SpecSection): Json {
return this.meta
}
}
export abstract class ReferenceSource<SpecSection> extends BaseSource {
@@ -186,9 +189,14 @@ export class ClientLibReferenceLoader extends ReferenceLoader<IFunctionDefinitio
return spec.functions
}
matchSpecSection(functionDefinitions: IFunctionDefinition[], id: string): IFunctionDefinition {
return functionDefinitions.find((functionDefinition) => functionDefinition.id === id)
}
enhanceMeta(section: IFunctionDefinition): Json {
return { ...this.meta, slug: section.id, methodName: section.title }
}
}
export class ClientLibReferenceSource extends ReferenceSource<IFunctionDefinition> {
+1
View File
@@ -552,6 +552,7 @@ export type Database = {
type: string
href: string
content: string
metadata: Json
subsections: Json[]
}[]
}
@@ -0,0 +1,56 @@
-- Alter the search_content function to also return the page metadata
drop function search_content;
create or replace function search_content(
embedding vector(1536),
include_full_content boolean default false,
match_threshold float default 0.78,
max_result int default 30
)
returns table (
id bigint,
page_title text,
type text,
href text,
content text,
metadata json,
subsections json[]
)
language sql
set search_path = ''
as $$
with matched_section as (
select
*,
row_number() over () as ranking
from public.match_embedding(
embedding,
match_threshold,
max_result
)
)
select
page.id,
meta ->> 'title' as page_title,
type,
public.get_full_content_url(type, path, null) as href,
case
when include_full_content
then page.content
else
null
end as content,
meta as metadata,
array_agg(
json_build_object(
'title', heading,
'href', public.get_full_content_url(type, path, slug),
'content', matched_section.content
)
)
from matched_section
join public.page on matched_section.page_id = page.id
group by page.id
order by min(ranking);
$$;