feat: updated integration page with missing API routes

This commit is contained in:
Florian Sylvain committed 2026-10-26 02:18:35 +01:00
1 parent 25d9c30b8a
commit 7c0ee21906
25 files changed
+2402 -350

No files matched your search

+564 -114
View File
@@ -1,4 +1,4 @@
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE'
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
export type AccessLevel = 'Public' | 'Verified user' | 'Owner or admin' | 'Owner only'
export interface ApiField {
@@ -11,6 +11,8 @@ export interface ApiField {
export interface ApiError {
status: number
caption: string
literal?: boolean
tail?: string
}
export type BodyKind = 'none' | 'json' | 'multipart'
@@ -30,12 +32,29 @@ export interface ApiEndpoint {
response: string
status: number
rateLimited: boolean
prettyBody: boolean
searchKeys: string[]
}
export interface FieldTypeRow {
type: string
value: string
rules: string
}
export interface GroupSnippet {
key: string
label: string
code: string
}
export interface ApiGroup {
key: string
title: string
description: string
note?: string
fieldTypes?: FieldTypeRow[]
snippet?: GroupSnippet
endpoints: ApiEndpoint[]
}
@@ -61,6 +80,7 @@ export interface ErrorRow {
status: number
meaning: string
messages: string[]
codes?: string[]
note?: string
}
@@ -75,6 +95,8 @@ export const NO_CONTENT_RESPONSE = '204 No Content'
export const SESSION_REQUIRED_MESSAGE = 'This action needs a real session.'
export const OWNER_REQUIRED_MESSAGE = 'Only the owner can do this.'
export const OWNER_OR_ADMIN_MESSAGE = 'Only owners and admins can do this.'
export const IMAGES_AFTER_GROUP = 'sites'
export const FIELD_TYPES_ANCHOR = 'ct-fields'
export const LOGIN_FAMILY_CAPTION = 'Made for the browser. A key is ignored here, with no 403.'
export const RATE_LIMIT_SENTENCE = 'This route is rate limited. Too many requests answer 429.'
@@ -302,6 +324,8 @@ const article = (overrides: Record<string, unknown> = {}) => ({
})
const SAMPLE_SITE_KEY = 'renew-blog'
const SAMPLE_CONTENT_TYPE_KEY = 'events'
const SAMPLE_BUILTIN_TYPE_KEY = 'author'
const resolvedSeo = () => ({
title: `${ARTICLE_TITLE} | Renew`,
@@ -452,6 +476,127 @@ const author = (overrides: Record<string, unknown> = {}) => ({
...overrides,
})
const fieldOf = (
key: string,
label: string,
type: string,
overrides: Record<string, unknown> = {},
) => ({
key,
label,
type,
required: false,
unique: false,
default: null,
min: null,
max: null,
options: null,
relation: null,
...overrides,
})
const eventFields = [
fieldOf('title', 'Title', 'text', { required: true, min: 3, max: 120 }),
fieldOf('starts_on', 'Starts on', 'date', { required: true }),
fieldOf('capacity', 'Capacity', 'number', { required: true, min: 1, max: 5000 }),
fieldOf('online', 'Online', 'boolean'),
fieldOf('category', 'Category', 'select', { options: ['Workshop', 'Talk', 'Open day'] }),
fieldOf('cover', 'Cover', 'media'),
]
const eventType = {
id: 2,
key: 'events',
name: 'Events',
icon: 'calendar',
fields: eventFields,
created_at: '2026-09-02T08:30:00Z',
updated_at: '2026-10-20T14:05:00Z',
}
const eventData = {
title: 'Open day',
starts_on: '2026-11-14',
capacity: 120,
online: false,
category: 'Open day',
cover: 4,
}
const eventEntry = (data: Record<string, unknown> = eventData) => ({
id: 31,
content_type: 'events',
data,
created_by: 1,
updated_by: 1,
created_at: '2026-10-20T14:05:00Z',
updated_at: '2026-10-22T09:41:00Z',
media: {
cover: {
id: 4,
filename: 'north-wall.jpg',
width: 1600,
height: 1067,
size: 3900000,
thumbnail_url: '/static/uploadedImages/3f9c-thumb.jpg',
},
},
relations: {},
})
const articleBuiltinFields = [
fieldOf('title', 'Title', 'text', { required: true, min: 3, max: 200 }),
fieldOf('slug', 'Slug', 'text', { required: true, unique: true }),
fieldOf('body', 'Body', 'richtext'),
]
const authorBuiltinFields = [
fieldOf('name', 'Name', 'text', { required: true, min: 1, max: 100 }),
fieldOf('slug', 'Slug', 'text', { required: true, unique: true }),
]
const FIELD_TYPE_ROWS: FieldTypeRow[] = [
{
type: 'text',
value: 'string',
rules: 'min/max = length in characters. Can be unique.',
},
{
type: 'richtext',
value: 'string (Markdown)',
rules: 'min/max = length. Cannot be unique.',
},
{ type: 'number', value: 'number', rules: 'min/max = value. Can be unique.' },
{ type: 'boolean', value: 'true/false', rules: 'No min/max. Cannot be unique.' },
{
type: 'date',
value: 'string "YYYY-MM-DD"',
rules: 'min/max = earliest/latest date. Can be unique.',
},
{
type: 'select',
value: 'one string from `options` (1 to 50 choices)',
rules: 'Can be unique.',
},
{
type: 'media',
value: 'the numeric id of an uploaded image (see Media)',
rules: '\u2014',
},
{
type: 'relation',
value: 'ids of entries of another content type; `relation: { content_type, kind }`, kind one_to_one | one_to_many | many_to_many',
rules: '\u2014',
},
]
const NAME_RULE = 'Up to 100 characters.'
const FIELDS_RULE = '1 to 50 fields, in order. See Field types.'
const ENTRY_DATA_RULE =
"Keys are the type's field keys. For events: title, starts_on, capacity, online, category and cover."
const ENTRY_SEARCH_KEYS = ['title', 'starts_on', 'capacity', 'online', 'category', 'cover']
const INVALID_PAGE_SIZE_SORT = 'Invalid page, size or sort.'
const me = {
id: 1,
username: 'marta',
@@ -475,10 +620,27 @@ const invite = (overrides: Record<string, unknown> = {}) => ({
const e = (status: number, caption: string): ApiError => ({ status, caption })
const literal = (status: number, caption: string, tail?: string): ApiError => ({
status,
caption,
literal: true,
...(tail ? { tail } : {}),
})
const e401 = e(401, UNAUTHORIZED)
const eOwnerOrAdmin = e(403, OWNER_OR_ADMIN_MESSAGE)
const contentTypeNotFound = literal(404, 'content_type_not_found')
const entryNotFound = literal(404, 'entry_not_found')
const invalidEntryUnique = literal(409, 'invalid_entry: title must be unique')
const invalidEntryRange = literal(422, 'invalid_entry: capacity must be at most 5000')
const articleErrors = (...own: ApiError[]) => [...own, e(400, FORMAT_INVALID), e(401, UNAUTHORIZED)]
const verified = (errors: ApiError[]) => [...errors, e(403, VERIFY_REQUIRED)]
const verifiedAfterLogin = (errors: ApiError[]) =>
errors.flatMap((error) => (error.status === 401 ? [error, e(403, VERIFY_REQUIRED)] : [error]))
const ownerOrAdminErrors = (...own: ApiError[]) => [
...own,
e(401, UNAUTHORIZED),
@@ -545,6 +707,8 @@ function endpoint(
response,
status: response === NO_CONTENT_RESPONSE ? 204 : 200,
rateLimited: false,
prettyBody: false,
searchKeys: [],
...extra,
}
}
@@ -843,6 +1007,255 @@ export const API_GROUPS: ApiGroup[] = [
),
],
},
{
key: 'ctypes',
title: 'Content types',
description:
'Define your own kind of content, like events or recipes. An owner or admin creates a type made of 1 to 50 ordered fields; the type then has its own entry routes.',
note: 'A content type has a `key` (2 to 40 lowercase letters, digits or hyphens, chosen at creation and never changed) and fields. Each field has a `key` (letters, digits and underscores, starting with a letter), a `label`, a `type` and rules.',
fieldTypes: FIELD_TYPE_ROWS,
snippet: { key: 'field', label: 'Field object', code: json(eventFields[2]) },
endpoints: [
endpoint(
'ct-list',
'GET',
'/v1/content-type/',
'List all custom content types.',
'Verified user',
verifiedAfterLogin([e401]),
json([{ ...eventType, fields: eventFields.slice(0, 3) }]),
),
endpoint(
'ct-get',
'GET',
'/v1/content-type/{key}',
'Get one content type with its fields.',
'Verified user',
verifiedAfterLogin([e401, contentTypeNotFound]),
json(eventType),
),
endpoint(
'ct-create',
'POST',
'/v1/content-type/',
'Create a content type.',
'Owner or admin',
[
e(400, BODY_MISMATCH),
e401,
eOwnerOrAdmin,
literal(409, 'content_type_key_taken'),
],
json(eventType),
{
status: 201,
prettyBody: true,
bodyKind: 'json',
body: [
{
name: 'key',
type: 'string',
required: true,
rule: "2 to 40 characters. Lowercase letters, digits and hyphens. Can't be changed later.",
},
{ name: 'name', type: 'string', required: true, rule: NAME_RULE },
{ name: 'icon', type: 'string', required: false, rule: 'Optional.' },
{ name: 'fields', type: 'object[]', required: true, rule: FIELDS_RULE },
],
bodySample: {
key: 'events',
name: 'Events',
icon: 'calendar',
fields: eventFields.slice(0, 3),
},
},
),
endpoint(
'ct-update',
'PUT',
'/v1/content-type/{key}',
"Replace the name, icon and fields of a type. The key never changes. Saved field keys and types can't change.",
'Owner or admin',
[
e(400, BODY_MISMATCH),
e401,
eOwnerOrAdmin,
contentTypeNotFound,
literal(
409,
'schema_incompatible',
"Some existing entries don't fit the new fields. Nothing was changed.",
),
],
json(eventType),
{
prettyBody: true,
bodyKind: 'json',
body: [
{ name: 'name', type: 'string', required: true, rule: NAME_RULE },
{ name: 'icon', type: 'string', required: false, rule: 'Optional.' },
{ name: 'fields', type: 'object[]', required: true, rule: FIELDS_RULE },
],
bodySample: {
name: 'Events',
icon: 'calendar',
fields: eventFields.slice(0, 3),
},
},
),
endpoint(
'ct-delete',
'DELETE',
'/v1/content-type/{key}',
'Delete a content type. Only possible when it has no entries and no relation field points at it.',
'Owner or admin',
[
e401,
eOwnerOrAdmin,
contentTypeNotFound,
literal(409, 'content_type_has_entries'),
literal(409, 'content_type_referenced'),
],
NO_CONTENT_RESPONSE,
),
],
},
{
key: 'entries',
title: 'Entries',
description:
'Read and write the content of a custom type. Any verified user can write entries.',
note: "An entry's `data` is an object keyed by field key, validated against the type. Responses add `media` (the resolved images for media fields, keyed by field key: id, filename, width, height, size and thumbnail_url, or { id, missing: true } if the image was deleted) and `relations` (the resolved first field of related entries).",
snippet: { key: 'entry', label: 'Entry', code: json(eventEntry()) },
endpoints: [
endpoint(
'en-list',
'GET',
'/v1/content-type/{key}/entry',
'List entries of a type, one page at a time. The response is an object with items, total, page and size, not a bare array like articles.',
'Verified user',
verifiedAfterLogin([e(400, INVALID_PAGE_SIZE_SORT), e401, contentTypeNotFound]),
json({ items: [eventEntry()], total: 1, page: 1, size: 25 }),
{
query: [
{
name: 'page',
type: 'integer',
required: false,
rule: '1 or higher. Default 1.',
},
{
name: 'size',
type: 'integer',
required: false,
rule: 'One of 5, 25, 50 or 100. Default 25.',
},
{
name: 'sort',
type: 'string',
required: false,
rule: 'id, created_at or updated_at, then :asc or :desc. For example updated_at:desc.',
},
],
},
),
endpoint(
'en-create',
'POST',
'/v1/content-type/{key}/entry',
'Create an entry.',
'Verified user',
verifiedAfterLogin([
e(400, BODY_MISMATCH),
e401,
contentTypeNotFound,
invalidEntryUnique,
invalidEntryRange,
]),
json(eventEntry()),
{
status: 201,
bodyKind: 'json',
searchKeys: ENTRY_SEARCH_KEYS,
body: [{ name: 'data', type: 'object', required: true, rule: ENTRY_DATA_RULE }],
bodySample: { data: eventData },
},
),
endpoint(
'en-get',
'GET',
'/v1/content-type/{key}/entry/{id}',
'Get one entry.',
'Verified user',
verifiedAfterLogin([e(400, ID_FORMAT), e401, entryNotFound, contentTypeNotFound]),
json(eventEntry()),
{ pathId: 31 },
),
endpoint(
'en-update',
'PUT',
'/v1/content-type/{key}/entry/{id}',
'Replace the data of an entry.',
'Verified user',
verifiedAfterLogin([
e(400, BODY_MISMATCH),
e401,
entryNotFound,
contentTypeNotFound,
invalidEntryUnique,
invalidEntryRange,
]),
json(eventEntry({ ...eventData, capacity: 150 })),
{
pathId: 31,
bodyKind: 'json',
searchKeys: ENTRY_SEARCH_KEYS,
body: [{ name: 'data', type: 'object', required: true, rule: ENTRY_DATA_RULE }],
bodySample: { data: { ...eventData, capacity: 150 } },
},
),
endpoint(
'en-delete',
'DELETE',
'/v1/content-type/{key}/entry/{id}',
'Delete an entry.',
'Verified user',
verifiedAfterLogin([e401, entryNotFound, contentTypeNotFound]),
NO_CONTENT_RESPONSE,
{ pathId: 31 },
),
],
},
{
key: 'builtin',
title: 'Built-in types',
description:
'The fields of Articles, Authors, Sites, Media and Users, in the same shape as the fields of a content type.',
note: 'Built-in types are read-only: they cannot be changed through the API.',
endpoints: [
endpoint(
'bt-list',
'GET',
'/v1/builtin-type/',
'List the built-in types (Articles, Authors, Sites, Media, Users) and their fields. Keys are article, author, site, media and user.',
'Owner or admin',
[e401, eOwnerOrAdmin],
json([
{ key: 'article', name: 'Articles', fields: articleBuiltinFields },
{ key: 'author', name: 'Authors', fields: authorBuiltinFields },
]),
),
endpoint(
'bt-get',
'GET',
'/v1/builtin-type/{key}',
'Get one built-in type. Keys are article, author, site, media and user.',
'Owner or admin',
[e401, eOwnerOrAdmin, literal(404, 'builtin_type_not_found')],
json({ key: 'author', name: 'Authors', fields: authorBuiltinFields }),
),
],
},
{
key: 'media',
title: 'Media',
@@ -1060,7 +1473,7 @@ export const API_GROUPS: ApiGroup[] = [
'med-purge-run',
'POST',
'/v1/media/purge',
'Merge every group of identical copies. Send the preview fingerprint to refuse a changed library.',
'Merge every group of identical copies. Send the preview fingerprint to refuse a changed library. failed_groups counts groups that could not be purged; the others were still purged.',
'Owner or admin',
ownerOrAdminErrors(
e(400, BODY_MISMATCH),
@@ -1073,6 +1486,7 @@ export const API_GROUPS: ApiGroup[] = [
merged: 2,
articles_updated: 5,
sites_updated: 1,
authors_updated: 0,
failed_groups: 0,
}),
{
@@ -1090,6 +1504,89 @@ export const API_GROUPS: ApiGroup[] = [
),
],
},
{
key: 'sites',
title: 'Sites and authors',
description:
'Read the sites and authors your content belongs to. Creating and changing them needs a session.',
endpoints: [
endpoint(
'site-list',
'GET',
'/v1/site/',
'List the sites, the default first and then by name, each with its article count. Without page or size every matching site is returned; the number of matching sites is in the X-Total-Count header.',
'Verified user',
verified([e(400, BODY_MISMATCH), e(401, UNAUTHORIZED)]),
json([
site(),
site({
id: 2,
name: 'Renew shop',
key: 'renew-shop',
base_url: 'https://shop.renew.example',
default_social_image_id: null,
default_robots: 'noindex',
locale: null,
is_default: false,
article_count: 4,
site_name: '',
title_template: '%title% - %site%',
default_description: '',
}),
]),
{
query: [
PAGE_QUERY,
SIZE_QUERY,
{
name: 'q',
type: 'string',
required: false,
rule: 'Up to 200 characters, matched in the name, the key or the base URL, ignoring case and accents.',
},
],
},
),
endpoint(
'author-list',
'GET',
'/v1/author/',
'List the authors by name, each with its article count. The total is also in the X-Total-Count header.',
'Verified user',
verified([e(400, 'The page, size or search is not valid.'), e(401, UNAUTHORIZED)]),
json({
items: [
author(),
author({
id: 7,
name: 'Camille Roche',
slug: 'camille-roche',
bio: 'Freelance writer on lime and stone.',
website: null,
user_id: null,
is_linked: false,
article_count: 3,
}),
],
total: 2,
page: 1,
size: 25,
}),
{
query: [
PAGE_QUERY,
SIZE_QUERY,
{
name: 'q',
type: 'string',
required: false,
rule: 'Up to 200 characters, matched in the name or the slug.',
},
],
},
),
],
},
{
key: 'users',
title: 'Users and invites',
@@ -1229,97 +1726,6 @@ export const API_GROUPS: ApiGroup[] = [
),
],
},
{
key: 'sites',
title: 'Sites',
description:
'The sites an article can belong to. Any verified user can read them; changes are made in the browser by the owner.',
endpoints: [
endpoint(
'site-list',
'GET',
'/v1/site/',
'List the sites, the default first and then by name, each with its article count. Without page or size every matching site is returned; the number of matching sites is in the X-Total-Count header.',
'Verified user',
verified([e(400, BODY_MISMATCH), e(401, UNAUTHORIZED)]),
json([
site(),
site({
id: 2,
name: 'Renew shop',
key: 'renew-shop',
base_url: 'https://shop.renew.example',
default_social_image_id: null,
default_robots: 'noindex',
locale: null,
is_default: false,
article_count: 4,
site_name: '',
title_template: '%title% - %site%',
default_description: '',
}),
]),
{
query: [
PAGE_QUERY,
SIZE_QUERY,
{
name: 'q',
type: 'string',
required: false,
rule: 'Up to 200 characters, matched in the name, the key or the base URL, ignoring case and accents.',
},
],
},
),
],
},
{
key: 'authors',
title: 'Authors',
description:
'The people credited on articles. Any verified user can read them; changes are made in the browser by an owner or admin.',
endpoints: [
endpoint(
'author-list',
'GET',
'/v1/author/',
'List the authors by name, each with its article count. The total is also in the X-Total-Count header.',
'Verified user',
verified([e(400, 'The page, size or search is not valid.'), e(401, UNAUTHORIZED)]),
json({
items: [
author(),
author({
id: 7,
name: 'Camille Roche',
slug: 'camille-roche',
bio: 'Freelance writer on lime and stone.',
website: null,
user_id: null,
is_linked: false,
article_count: 3,
}),
],
total: 2,
page: 1,
size: 25,
}),
{
query: [
PAGE_QUERY,
SIZE_QUERY,
{
name: 'q',
type: 'string',
required: false,
rule: 'Up to 200 characters, matched in the name or the slug.',
},
],
},
),
],
},
]
const row = (
@@ -1342,6 +1748,7 @@ export const SESSION_GROUPS: SessionGroup[] = [
row('POST', '/v1/system/update/check', 'Check for update'),
row('POST', '/v1/system/update/download', 'Download update'),
row('POST', '/v1/system/update/restart', 'Restart'),
row('PATCH', '/v1/settings', 'Change the activity log retention'),
],
},
{
@@ -1356,17 +1763,20 @@ export const SESSION_GROUPS: SessionGroup[] = [
],
},
{
title: 'Sites · owner only',
title: 'API key management',
rows: [
row('GET', '/v1/api-key', 'List keys'),
row('POST', '/v1/api-key', 'Create a key'),
row('DELETE', '/v1/api-key/{id}', 'Revoke a key'),
],
},
{
title: 'Sites and authors writes',
rows: [
row('POST', '/v1/site/', 'Create a site'),
row('PATCH', '/v1/site/{id}', 'Edit a site'),
row('PATCH', '/v1/site/{id}/default', 'Make a site the default'),
row('DELETE', '/v1/site/{id}', 'Delete a site, moving its articles with reassign_to'),
],
},
{
title: 'Authors · owner and admin',
rows: [
row('POST', '/v1/author/', 'Create an author'),
row(
'PATCH',
@@ -1382,25 +1792,30 @@ export const SESSION_GROUPS: SessionGroup[] = [
],
},
{
title: 'Settings · owner only',
rows: [row('PATCH', '/v1/settings', 'Change the activity log retention')],
title: 'Image variants',
rows: [
row('GET', '/v1/image-variants/', 'List variants'),
row('POST', '/v1/image-variants/', 'Create a variant'),
row('PUT', '/v1/image-variants/{name}', 'Replace a variant'),
row('PATCH', '/v1/image-variants/{name}', 'Update a variant'),
row('DELETE', '/v1/image-variants/{name}', 'Delete a variant'),
row('POST', '/v1/image-variants/{name}/purge', 'Purge the cache of a variant'),
row('GET', '/v1/image-variants/cache', 'Image cache status'),
row('POST', '/v1/image-variants/cache/purge', 'Purge the image cache'),
row('GET', '/v1/media/{id}/variants', 'List the variants of an image'),
row('POST', '/v1/media/{id}/variants', 'Add variants to an image'),
row('POST', '/v1/media/{id}/variants/{name}', 'Generate one variant of an image'),
row('DELETE', '/v1/media/{id}/variants/{name}', 'Delete one variant of an image'),
],
},
{
title: 'Activity log',
title: 'Activity',
rows: [
row('GET', '/v1/activity', 'List activity events'),
row('GET', '/v1/activity/summary', 'Activity summary, owner and admin'),
row('GET', '/v1/activity/export', 'Export activity as CSV, owner and admin'),
],
},
{
title: 'API key management',
rows: [
row('GET', '/v1/api-key', 'List keys'),
row('POST', '/v1/api-key', 'Create a key'),
row('DELETE', '/v1/api-key/{id}', 'Revoke a key'),
],
},
]
export const PUBLIC_AUTH_ROWS: PublicRow[] = [
@@ -1442,13 +1857,20 @@ export const ERROR_ROWS: ErrorRow[] = [
{
status: 403,
meaning: 'Forbidden',
messages: [SESSION_REQUIRED_MESSAGE, OWNER_REQUIRED_MESSAGE, VERIFY_REQUIRED],
messages: [
SESSION_REQUIRED_MESSAGE,
OWNER_OR_ADMIN_MESSAGE,
OWNER_REQUIRED_MESSAGE,
VERIFY_REQUIRED,
],
note: 'Site SEO defaults are owner only. Article SEO fields need only the right to edit the article.',
},
{
status: 404,
meaning: 'Not found',
messages: ['Article not found', 'Image not found', 'Invite not found', 'API key not found'],
codes: ['content_type_not_found', 'entry_not_found', 'builtin_type_not_found'],
note: 'Text in code style is the literal body of the response, not a sentence.',
},
{
status: 409,
@@ -1456,7 +1878,25 @@ export const ERROR_ROWS: ErrorRow[] = [
messages: [
"There's already a pending invite for this address.",
'This invite has already been accepted.',
'Some of the images to merge changed. Refresh and try again.',
'This address already has an account.',
],
codes: [
'content_type_key_taken',
'content_type_has_entries',
'content_type_referenced',
'schema_incompatible',
'invalid_entry: <reason>',
'slug_taken',
],
note: 'On an entry, `invalid_entry: <reason>` is a 409 only when a unique field clashes. Any other field error is a 422.',
},
{
status: 422,
meaning: 'Unprocessable',
messages: [],
codes: ['invalid_entry: <reason>'],
note: 'An entry doesn\'t fit its content type. The body is `invalid_entry`, then ": " and the reason, for example `invalid_entry: capacity must be at most 5000`.',
},
{
status: 429,
@@ -1474,8 +1914,15 @@ export const ERROR_SAMPLE = `HTTP/1.1 403 Forbidden\nContent-Type: text/plain\n\
export const AUTH_EXAMPLE_PATH = '/v1/auth/me'
function sampleKey(path: string) {
if (path.startsWith('/v1/content-type')) {
return SAMPLE_CONTENT_TYPE_KEY
}
return path.startsWith('/v1/builtin-type') ? SAMPLE_BUILTIN_TYPE_KEY : SAMPLE_SITE_KEY
}
function resolvedPath(item: Pick<ApiEndpoint, 'path' | 'pathId'>) {
const keyed = item.path.replace('{key}', SAMPLE_SITE_KEY)
const keyed = item.path.replace('{key}', sampleKey(item.path))
return item.pathId === null ? keyed : keyed.replace('{id}', String(item.pathId))
}
@@ -1485,12 +1932,15 @@ export function endpointExamples(item: ApiEndpoint, origin: string): EndpointExa
const isJson = item.bodyKind === 'json' && item.bodySample !== null
const isForm = item.bodyKind === 'multipart'
const authHeader = `Authorization: Bearer ${API_KEY_PLACEHOLDER}`
const bodyJson = item.prettyBody
? JSON.stringify(item.bodySample, null, 2)
: JSON.stringify(item.bodySample)
const curlParts = [`curl ${item.method === 'GET' ? '' : `-X ${item.method} `}"${url}"`]
if (!isPublic) curlParts.push(` -H "${authHeader}"`)
if (isJson) {
curlParts.push(` -H "Content-Type: application/json"`)
curlParts.push(` -d '${JSON.stringify(item.bodySample)}'`)
curlParts.push(` -d '${bodyJson}'`)
}
if (isForm) curlParts.push(` -F "file=@north-wall.jpg"`)
@@ -1501,7 +1951,7 @@ export function endpointExamples(item: ApiEndpoint, origin: string): EndpointExa
isJson ? `'Content-Type': 'application/json'` : null,
].filter((header) => header !== null)
if (headers.length > 0) options.push(`headers: { ${headers.join(', ')} }`)
if (isJson) options.push(`body: JSON.stringify(${JSON.stringify(item.bodySample)})`)
if (isJson) options.push(`body: JSON.stringify(${bodyJson.replace(/\n/g, '\n ')})`)
if (isForm) options.push('body: form')
const formSetup = isForm ? "const form = new FormData()\nform.append('file', file)\n\n" : ''