feat: updated integration page with missing API routes
This commit is contained in:
1 parent
25d9c30b8a
commit
7c0ee21906
25 files changed
+2402
-350
No files matched your search
@@ -51,18 +51,27 @@ describe('index', () => {
|
||||
'auth',
|
||||
'articles',
|
||||
'seo',
|
||||
'ctypes',
|
||||
'entries',
|
||||
'builtin',
|
||||
'media',
|
||||
'sites',
|
||||
'images',
|
||||
'users',
|
||||
'settings',
|
||||
'sites',
|
||||
'authors',
|
||||
'images',
|
||||
'session',
|
||||
'errors',
|
||||
'notes',
|
||||
])
|
||||
})
|
||||
|
||||
it('holds 85 endpoints: 42 documented, 37 session only and 6 public auth', () => {
|
||||
expect(index.endpoints).toHaveLength(85)
|
||||
expect(index.endpoints.filter((entry) => entry.cardId !== null)).toHaveLength(42)
|
||||
expect(index.endpoints.filter((entry) => entry.session)).toHaveLength(37)
|
||||
expect(index.endpoints.filter((entry) => entry.key.startsWith('public:'))).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('never indexes code samples or response examples', () => {
|
||||
expect(search('rcms_kf3x').endpoints.map((hit) => hit.entry.key)).toEqual([])
|
||||
expect(search('"total"').endpoints).toEqual([])
|
||||
@@ -164,12 +173,13 @@ describe('matching', () => {
|
||||
it('finds sections by title, description and keywords', () => {
|
||||
expect(routes(search('media').sections)).toEqual(['section Media'])
|
||||
expect(search('media').sections[0]!.entry).toMatchObject({ description: '11 routes' })
|
||||
expect(search('media').sections[0]!.entry.sectionId).toBe('media')
|
||||
const login = search('login')
|
||||
expect(routes(login.endpoints)).toEqual(['POST /v1/auth/login'])
|
||||
expect(routes(login.sections)).toEqual(['section Session only'])
|
||||
expect(login.sections[0]!.entry).toMatchObject({
|
||||
locked: true,
|
||||
description: '25 routes, not available to API keys',
|
||||
description: '37 routes, not available to API keys',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -189,6 +199,58 @@ describe('matching', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom content types', () => {
|
||||
it.each([
|
||||
['media', 16, 1],
|
||||
['404', 23, 1],
|
||||
['login', 1, 1],
|
||||
['entry', 7, 1],
|
||||
['capacity', 2, 0],
|
||||
['schema_incompatible', 1, 0],
|
||||
['content type', 10, 2],
|
||||
])('counts "%s" as %i endpoints and %i sections', (query, endpoints, sections) => {
|
||||
const result = search(query)
|
||||
expect(result.endpoints).toHaveLength(endpoints)
|
||||
expect(result.sections).toHaveLength(sections)
|
||||
})
|
||||
|
||||
it('also matches "entries" for the term entry, in text and in highlights', () => {
|
||||
const keys = search('entry').endpoints.map((hit) => hit.entry.key)
|
||||
expect(keys).toContain('ep:ct-delete')
|
||||
expect(keys).toContain('ep:ct-update')
|
||||
expect(routes(search('entry').sections)).toEqual(['section Entries'])
|
||||
expect(highlightSegments('Entries and entry', ['entry'])).toEqual([
|
||||
{ text: 'Entries', marked: true },
|
||||
{ text: ' and ', marked: false },
|
||||
{ text: 'entry', marked: true },
|
||||
])
|
||||
})
|
||||
|
||||
it('labels the events keys as body fields and a code as an error', () => {
|
||||
const capacity = search('capacity').endpoints
|
||||
expect(capacity.map((hit) => hit.via?.kind)).toEqual(['body', 'body'])
|
||||
expect(search('starts_on').endpoints.map((hit) => hit.via)).toEqual([
|
||||
{ kind: 'body', text: 'starts_on' },
|
||||
{ kind: 'body', text: 'starts_on' },
|
||||
])
|
||||
const [schema] = search('schema_incompatible').endpoints
|
||||
expect(schema!.via!.kind).toBe('error')
|
||||
expect(schema!.via!.text.startsWith('409 schema_incompatible')).toBe(true)
|
||||
expect(routes(search('schema_incompatible').endpoints)).toEqual([
|
||||
'PUT /v1/content-type/{key}',
|
||||
])
|
||||
})
|
||||
|
||||
it('finds the new sections by their keywords', () => {
|
||||
expect(routes(search('schema').sections)).toEqual([
|
||||
'section Content types',
|
||||
'section Built-in types',
|
||||
])
|
||||
expect(routes(search('author').sections)).toContain('section Sites and authors')
|
||||
expect(routes(search('purge').sections)).toEqual(['section Media'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('suggestions', () => {
|
||||
it('lists the five suggested rows in order', () => {
|
||||
expect(routes(suggestedHits(index))).toEqual([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HttpMethod } from '@/data/apiCatalogue'
|
||||
import { IMAGES_AFTER_GROUP, type ApiGroup, type HttpMethod } from '@/data/apiCatalogue'
|
||||
|
||||
export const SECTION_TITLE = 'text-[1.125rem]/6.5 font-semibold text-highlighted'
|
||||
|
||||
@@ -16,10 +16,13 @@ const METHOD_BASE =
|
||||
|
||||
export const SESSION_METHOD_CLASS = `${METHOD_BASE} border-dashed border-accented bg-transparent text-muted`
|
||||
|
||||
const PATCH_CLASS = `${METHOD_BASE} border-warning bg-(--rc-warning-soft) text-warning`
|
||||
|
||||
export const METHOD_CLASS: Record<HttpMethod, string> = {
|
||||
GET: `${METHOD_BASE} border-inverted bg-inverted text-inverted`,
|
||||
POST: `${METHOD_BASE} border-success bg-(--rc-success-soft) text-success`,
|
||||
PATCH: `${METHOD_BASE} border-warning bg-(--rc-warning-soft) text-warning`,
|
||||
PATCH: PATCH_CLASS,
|
||||
PUT: PATCH_CLASS,
|
||||
DELETE: `${METHOD_BASE} border-error bg-(--rc-error-soft) text-error`,
|
||||
}
|
||||
|
||||
@@ -30,3 +33,8 @@ export function splitInlineCode(text: string) {
|
||||
export function endpointAnchor(id: string) {
|
||||
return `ep-${id}`
|
||||
}
|
||||
|
||||
export function groupsAroundImages(groups: ApiGroup[]) {
|
||||
const split = groups.findIndex((group) => group.key === IMAGES_AFTER_GROUP) + 1
|
||||
return { before: groups.slice(0, split), after: groups.slice(split) }
|
||||
}
|
||||
+57
-11
@@ -1,10 +1,14 @@
|
||||
import { t, translate } from '@/i18n'
|
||||
import { groupsAroundImages } from '@/lib/apiReference'
|
||||
import {
|
||||
API_GROUPS,
|
||||
ERROR_ROWS,
|
||||
LOGIN_FAMILY_CAPTION,
|
||||
PUBLIC_AUTH_ROWS,
|
||||
SESSION_GROUPS,
|
||||
type ApiEndpoint,
|
||||
type ApiError,
|
||||
type ApiGroup,
|
||||
type HttpMethod,
|
||||
} from '@/data/apiCatalogue'
|
||||
|
||||
@@ -63,6 +67,18 @@ export interface SearchIndex {
|
||||
sections: SectionEntry[]
|
||||
}
|
||||
|
||||
const ENTRY_TERM = 'entry'
|
||||
const ENTRY_PLURAL = 'entries'
|
||||
|
||||
const GROUP_KEYWORDS: Record<string, string> = {
|
||||
seo: 'seo sitemap',
|
||||
ctypes: 'custom fields field types schema',
|
||||
entries: 'content type custom data',
|
||||
builtin: 'builtin fields schema',
|
||||
media: 'purge',
|
||||
sites: 'site author',
|
||||
}
|
||||
|
||||
const SUGGESTED_KEYS = ['section:auth', 'ep:med-up', 'section:errors', 'ep:art-list', 'ep:usr-me']
|
||||
|
||||
export function firstSentence(text: string) {
|
||||
@@ -74,6 +90,17 @@ export function queryTerms(query: string) {
|
||||
return query.toLowerCase().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
function errorText(error: ApiError) {
|
||||
const caption = translate(error.caption)
|
||||
return error.tail
|
||||
? `${error.status} ${caption} ${translate(error.tail)}`
|
||||
: `${error.status} ${caption}`
|
||||
}
|
||||
|
||||
function bodyKeys(endpoint: ApiEndpoint) {
|
||||
return [...new Set([...endpoint.body.map((field) => field.name), ...endpoint.searchKeys])]
|
||||
}
|
||||
|
||||
function endpointEntries(): EndpointEntry[] {
|
||||
const documented = API_GROUPS.flatMap((group) =>
|
||||
group.endpoints.map<EndpointEntry>((endpoint) => ({
|
||||
@@ -91,11 +118,11 @@ function endpointEntries(): EndpointEntry[] {
|
||||
kind: 'query',
|
||||
text: field.name,
|
||||
})),
|
||||
...endpoint.body.map<SearchMatch>((field) => ({ kind: 'body', text: field.name })),
|
||||
...bodyKeys(endpoint).map<SearchMatch>((text) => ({ kind: 'body', text })),
|
||||
],
|
||||
errors: endpoint.errors.map<SearchMatch>((error) => ({
|
||||
kind: 'error',
|
||||
text: `${error.status} ${translate(error.caption)}`,
|
||||
text: errorText(error),
|
||||
})),
|
||||
})),
|
||||
)
|
||||
@@ -154,6 +181,14 @@ function sectionEntries(): SectionEntry[] {
|
||||
locked,
|
||||
keywords,
|
||||
})
|
||||
const { before, after } = groupsAroundImages(API_GROUPS)
|
||||
const groupSection = (group: ApiGroup) =>
|
||||
section(
|
||||
group.key,
|
||||
translate(group.title),
|
||||
routeCount(group.endpoints.length),
|
||||
GROUP_KEYWORDS[group.key],
|
||||
)
|
||||
return [
|
||||
section(
|
||||
'auth',
|
||||
@@ -161,15 +196,14 @@ function sectionEntries(): SectionEntry[] {
|
||||
t('Public access and API keys'),
|
||||
'bearer api key rcms rate limit 429 base url public',
|
||||
),
|
||||
...API_GROUPS.map((group) =>
|
||||
section(group.key, translate(group.title), routeCount(group.endpoints.length)),
|
||||
),
|
||||
...before.map(groupSection),
|
||||
section(
|
||||
'images',
|
||||
t('Images and variants'),
|
||||
t('The ?v= parameter'),
|
||||
'variant image width height format quality',
|
||||
),
|
||||
...after.map(groupSection),
|
||||
section(
|
||||
'session',
|
||||
t('Session only'),
|
||||
@@ -196,19 +230,23 @@ export function buildSearchIndex(): SearchIndex {
|
||||
return { endpoints: endpointEntries(), sections: sectionEntries() }
|
||||
}
|
||||
|
||||
function includesTerm(text: string, term: string) {
|
||||
return text.includes(term) || (term === ENTRY_TERM && text.includes(ENTRY_PLURAL))
|
||||
}
|
||||
|
||||
function primaryText(entry: EndpointEntry) {
|
||||
return `${entry.method} ${entry.path} ${entry.description} ${entry.access}`.toLowerCase()
|
||||
}
|
||||
|
||||
function viaMatch(entry: EndpointEntry, term: string): SearchMatch | null | undefined {
|
||||
if (primaryText(entry).includes(term)) {
|
||||
if (includesTerm(primaryText(entry), term)) {
|
||||
return null
|
||||
}
|
||||
const field = entry.fields.find((item) => item.text.toLowerCase().includes(term))
|
||||
const field = entry.fields.find((item) => includesTerm(item.text.toLowerCase(), term))
|
||||
if (field) {
|
||||
return field
|
||||
}
|
||||
return entry.errors.find((item) => item.text.toLowerCase().includes(term))
|
||||
return entry.errors.find((item) => includesTerm(item.text.toLowerCase(), term))
|
||||
}
|
||||
|
||||
function matchEndpoint(entry: EndpointEntry, terms: string[]): SearchHit | null {
|
||||
@@ -225,7 +263,7 @@ function matchEndpoint(entry: EndpointEntry, terms: string[]): SearchHit | null
|
||||
|
||||
function matchSection(entry: SectionEntry, terms: string[]): SearchHit | null {
|
||||
const text = `${entry.title} ${entry.description} ${entry.keywords}`.toLowerCase()
|
||||
return terms.every((term) => text.includes(term)) ? { entry, via: null } : null
|
||||
return terms.every((term) => includesTerm(text, term)) ? { entry, via: null } : null
|
||||
}
|
||||
|
||||
export function suggestedHits(index: SearchIndex): SearchHit[] {
|
||||
@@ -254,14 +292,22 @@ function escapePattern(term: string) {
|
||||
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function termPattern(term: string) {
|
||||
return term === ENTRY_TERM ? 'entr(?:y|ies)' : escapePattern(term)
|
||||
}
|
||||
|
||||
function isMarked(part: string, terms: Set<string>) {
|
||||
return terms.has(part) || (part === ENTRY_PLURAL && terms.has(ENTRY_TERM))
|
||||
}
|
||||
|
||||
export function highlightSegments(text: string, terms: string[]): HighlightSegment[] {
|
||||
if (terms.length === 0) {
|
||||
return [{ text, marked: false }]
|
||||
}
|
||||
const pattern = new RegExp(`(${terms.map(escapePattern).join('|')})`, 'gi')
|
||||
const pattern = new RegExp(`(${terms.map(termPattern).join('|')})`, 'gi')
|
||||
const lowered = new Set(terms)
|
||||
return text
|
||||
.split(pattern)
|
||||
.filter((part) => part !== '')
|
||||
.map((part) => ({ text: part, marked: lowered.has(part.toLowerCase()) }))
|
||||
.map((part) => ({ text: part, marked: isMarked(part.toLowerCase(), lowered) }))
|
||||
}
|
||||
Reference in new issue
Block a user