mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-20 17:43:24 +00:00
v2: fix error handling in middleware and data fetcher (#2925)
This commit is contained in:
@@ -243,7 +243,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 8
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
"@gitbook/api": "0.96.1",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"next": "^15.2.0",
|
||||
"next": "^15.2.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
@@ -4280,7 +4280,7 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"gitbook-v2/next": ["next@15.2.0", "", { "dependencies": { "@next/env": "15.2.0", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0", "@next/swc-darwin-x64": "15.2.0", "@next/swc-linux-arm64-gnu": "15.2.0", "@next/swc-linux-arm64-musl": "15.2.0", "@next/swc-linux-x64-gnu": "15.2.0", "@next/swc-linux-x64-musl": "15.2.0", "@next/swc-win32-arm64-msvc": "15.2.0", "@next/swc-win32-x64-msvc": "15.2.0", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ=="],
|
||||
"gitbook-v2/next": ["next@15.2.1", "", { "dependencies": { "@next/env": "15.2.1", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.1", "@next/swc-darwin-x64": "15.2.1", "@next/swc-linux-arm64-gnu": "15.2.1", "@next/swc-linux-arm64-musl": "15.2.1", "@next/swc-linux-x64-gnu": "15.2.1", "@next/swc-linux-x64-musl": "15.2.1", "@next/swc-win32-arm64-msvc": "15.2.1", "@next/swc-win32-x64-msvc": "15.2.1", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g=="],
|
||||
|
||||
"google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
|
||||
|
||||
@@ -5180,23 +5180,23 @@
|
||||
|
||||
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.2.0", "", {}, "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA=="],
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.2.1", "", {}, "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw=="],
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw=="],
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g=="],
|
||||
|
||||
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "^15.2.0",
|
||||
"next": "^15.2.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@gitbook/api": "0.96.1",
|
||||
|
||||
@@ -3,8 +3,6 @@ import type { NextRequest } from 'next/server';
|
||||
import { serveIcon } from '@/routes/icon';
|
||||
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
SiteStructure,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data';
|
||||
import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env';
|
||||
@@ -164,11 +164,13 @@ export async function fetchSiteContextByURL(
|
||||
}
|
||||
): Promise<GitBookSiteContext> {
|
||||
const { dataFetcher } = baseContext;
|
||||
const data = await dataFetcher.getPublishedContentByUrl({
|
||||
url: input.url,
|
||||
visitorAuthToken: input.visitorAuthToken,
|
||||
redirectOnError: input.redirectOnError,
|
||||
});
|
||||
const data = await throwIfDataError(
|
||||
dataFetcher.getPublishedContentByUrl({
|
||||
url: input.url,
|
||||
visitorAuthToken: input.visitorAuthToken,
|
||||
redirectOnError: input.redirectOnError,
|
||||
})
|
||||
);
|
||||
|
||||
return fetchSiteContextByURLLookup(baseContext, data);
|
||||
}
|
||||
@@ -225,11 +227,13 @@ export async function fetchSiteContextByIds(
|
||||
|
||||
const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] =
|
||||
await Promise.all([
|
||||
dataFetcher.getPublishedContentSite({
|
||||
organizationId: ids.organization,
|
||||
siteId: ids.site,
|
||||
siteShareKey: ids.shareKey,
|
||||
}),
|
||||
throwIfDataError(
|
||||
dataFetcher.getPublishedContentSite({
|
||||
organizationId: ids.organization,
|
||||
siteId: ids.site,
|
||||
siteShareKey: ids.shareKey,
|
||||
})
|
||||
),
|
||||
fetchSpaceContextByIds(baseContext, ids),
|
||||
]);
|
||||
|
||||
@@ -302,27 +306,33 @@ export async function fetchSpaceContextByIds(
|
||||
const { dataFetcher } = baseContext;
|
||||
|
||||
const [space, changeRequest] = await Promise.all([
|
||||
dataFetcher.getSpace({
|
||||
spaceId: ids.space,
|
||||
shareKey: ids.shareKey,
|
||||
}),
|
||||
throwIfDataError(
|
||||
dataFetcher.getSpace({
|
||||
spaceId: ids.space,
|
||||
shareKey: ids.shareKey,
|
||||
})
|
||||
),
|
||||
ids.changeRequest
|
||||
? dataFetcher.getChangeRequest({
|
||||
spaceId: ids.space,
|
||||
changeRequestId: ids.changeRequest,
|
||||
})
|
||||
? throwIfDataError(
|
||||
dataFetcher.getChangeRequest({
|
||||
spaceId: ids.space,
|
||||
changeRequestId: ids.changeRequest,
|
||||
})
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
|
||||
const revisionId = changeRequest?.revision ?? ids.revision ?? space.revision;
|
||||
|
||||
const pages = await dataFetcher.getRevisionPages({
|
||||
spaceId: ids.space,
|
||||
revisionId,
|
||||
// We only care about the Git metadata when the Git sync is enabled,
|
||||
// otherwise we can optimize performance by not fetching it
|
||||
metadata: !!space.gitSync,
|
||||
});
|
||||
const pages = await throwIfDataError(
|
||||
dataFetcher.getRevisionPages({
|
||||
spaceId: ids.space,
|
||||
revisionId,
|
||||
// We only care about the Git metadata when the Git sync is enabled,
|
||||
// otherwise we can optimize performance by not fetching it
|
||||
metadata: !!space.gitSync,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...baseContext,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type ComputedContentSource, GitBookAPI } from '@gitbook/api';
|
||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
|
||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
import { wrapDataFetcherError } from './errors';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
interface DataFetcherInput {
|
||||
@@ -156,16 +157,10 @@ async function getUserById(input: DataFetcherInput, userId: string) {
|
||||
|
||||
cacheLife('days');
|
||||
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).users.getUserById(userId);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getSpace(
|
||||
@@ -185,10 +180,12 @@ async function getSpace(
|
||||
})
|
||||
);
|
||||
|
||||
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
|
||||
shareKey: params.shareKey,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
|
||||
shareKey: params.shareKey,
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function getChangeRequest(
|
||||
@@ -202,7 +199,7 @@ async function getChangeRequest(
|
||||
|
||||
cacheLife('minutes');
|
||||
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getChangeRequestById(
|
||||
params.spaceId,
|
||||
params.changeRequestId
|
||||
@@ -215,13 +212,7 @@ async function getChangeRequest(
|
||||
})
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getRevision(
|
||||
@@ -236,10 +227,12 @@ async function getRevision(
|
||||
|
||||
cacheLife('max');
|
||||
|
||||
const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function getRevisionPages(
|
||||
@@ -254,14 +247,16 @@ async function getRevisionPages(
|
||||
|
||||
cacheLife('max');
|
||||
|
||||
const res = await getAPI(input).spaces.listPagesInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
{
|
||||
metadata: params.metadata,
|
||||
}
|
||||
);
|
||||
return res.data.pages;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.listPagesInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
{
|
||||
metadata: params.metadata,
|
||||
}
|
||||
);
|
||||
return res.data.pages;
|
||||
});
|
||||
}
|
||||
|
||||
async function getRevisionFile(
|
||||
@@ -276,20 +271,14 @@ async function getRevisionFile(
|
||||
|
||||
cacheLife('max');
|
||||
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getFileInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.fileId
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getRevisionPageByPath(
|
||||
@@ -305,7 +294,7 @@ async function getRevisionPageByPath(
|
||||
cacheLife('max');
|
||||
|
||||
const encodedPath = encodeURIComponent(params.path);
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getPageInRevisionByPath(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
@@ -313,13 +302,7 @@ async function getRevisionPageByPath(
|
||||
);
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getDocument(
|
||||
@@ -333,8 +316,10 @@ async function getDocument(
|
||||
|
||||
cacheLife('max');
|
||||
|
||||
const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId);
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
|
||||
async function getComputedDocument(
|
||||
@@ -359,10 +344,12 @@ async function getComputedDocument(
|
||||
)
|
||||
);
|
||||
|
||||
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
|
||||
source: params.source,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
|
||||
source: params.source,
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function getReusableContent(
|
||||
@@ -377,20 +364,14 @@ async function getReusableContent(
|
||||
|
||||
cacheLife('max');
|
||||
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).spaces.getReusableContentInRevisionById(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.reusableContentId
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getLatestOpenAPISpecVersionContent(
|
||||
@@ -411,19 +392,13 @@ async function getLatestOpenAPISpecVersionContent(
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).orgs.getLatestOpenApiSpecVersionContent(
|
||||
params.organizationId,
|
||||
params.slug
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getPublishedContentByUrl(
|
||||
@@ -447,22 +422,24 @@ async function getPublishedContentByUrl(
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).urls.getPublishedContentByUrl({
|
||||
url,
|
||||
visitorAuthToken: visitorAuthToken ?? undefined,
|
||||
redirectOnError,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).urls.getPublishedContentByUrl({
|
||||
url,
|
||||
visitorAuthToken: visitorAuthToken ?? undefined,
|
||||
redirectOnError,
|
||||
});
|
||||
|
||||
if ('site' in res.data) {
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: res.data.site,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
});
|
||||
|
||||
if ('site' in res.data) {
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: res.data.site,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function getPublishedContentSite(
|
||||
@@ -483,14 +460,16 @@ async function getPublishedContentSite(
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).orgs.getPublishedContentSite(
|
||||
params.organizationId,
|
||||
params.siteId,
|
||||
{
|
||||
shareKey: params.siteShareKey,
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).orgs.getPublishedContentSite(
|
||||
params.organizationId,
|
||||
params.siteId,
|
||||
{
|
||||
shareKey: params.siteShareKey,
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
|
||||
async function getSiteRedirectBySource(
|
||||
@@ -512,7 +491,7 @@ async function getSiteRedirectBySource(
|
||||
);
|
||||
cacheLife('days');
|
||||
|
||||
try {
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).orgs.getSiteRedirectBySource(
|
||||
params.organizationId,
|
||||
params.siteId,
|
||||
@@ -523,19 +502,7 @@ async function getSiteRedirectBySource(
|
||||
);
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
// 422 is returned when the source is invalid
|
||||
// we don't want to throw but just return null
|
||||
if (checkHasErrorCode(error, 422)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getEmbedByUrl(
|
||||
@@ -549,9 +516,11 @@ async function getEmbedByUrl(
|
||||
|
||||
cacheLife('weeks');
|
||||
|
||||
const api = getAPI(input);
|
||||
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
|
||||
return res.data;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const api = getAPI(input);
|
||||
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
|
||||
async function searchSiteContent(
|
||||
@@ -564,11 +533,13 @@ async function searchSiteContent(
|
||||
|
||||
cacheLife('days');
|
||||
|
||||
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
|
||||
query,
|
||||
...scope,
|
||||
return wrapDataFetcherError(async () => {
|
||||
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
|
||||
query,
|
||||
...scope,
|
||||
});
|
||||
return res.data.items;
|
||||
});
|
||||
return res.data.items;
|
||||
}
|
||||
|
||||
function getAPI(input: DataFetcherInput) {
|
||||
@@ -581,7 +552,3 @@ function getAPI(input: DataFetcherInput) {
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function checkHasErrorCode(error: unknown, code: number) {
|
||||
return error instanceof Error && 'code' in error && error.code === code;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { GitBookAPIError } from '@gitbook/api';
|
||||
import type { DataFetcherErrorData, DataFetcherResponse } from './types';
|
||||
|
||||
export class DataFetcherError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: number
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error if the response contains an error.
|
||||
*/
|
||||
export function throwIfDataError<T>(response: DataFetcherResponse<T>): T;
|
||||
export function throwIfDataError<T>(response: Promise<DataFetcherResponse<T>>): Promise<T>;
|
||||
export function throwIfDataError<T>(
|
||||
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>
|
||||
): T | Promise<T> {
|
||||
if (response instanceof Promise) {
|
||||
return response.then((result) => throwIfDataError(result));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new DataFetcherError(response.error.message, response.error.code);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data from the response or null if there is an "Not found" error.
|
||||
*/
|
||||
export function getDataOrNull<T>(
|
||||
response: DataFetcherResponse<T>,
|
||||
ignoreErrors?: number[]
|
||||
): T | null;
|
||||
export function getDataOrNull<T>(
|
||||
response: Promise<DataFetcherResponse<T>>,
|
||||
ignoreErrors?: number[]
|
||||
): Promise<T | null>;
|
||||
export function getDataOrNull<T>(
|
||||
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
|
||||
ignoreErrors: number[] = [404]
|
||||
): T | null | Promise<T | null> {
|
||||
if (response instanceof Promise) {
|
||||
return response.then((result) => getDataOrNull(result, ignoreErrors));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (ignoreErrors.includes(response.error.code)) return null;
|
||||
throw new DataFetcherError(response.error.message, response.error.code);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore error for an API or data call.
|
||||
*/
|
||||
export async function ignoreDataThrownError<T>(promise: Promise<T>): Promise<T | null> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (error) {
|
||||
getExposableError(error as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore all errors for an API or data call.
|
||||
*/
|
||||
export async function ignoreAllThrownError<T>(promise: Promise<T>): Promise<T | null> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (error) {
|
||||
console.warn('Ignoring error', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an async execution to handle errors and return a DataFetcherResponse.
|
||||
*/
|
||||
export async function wrapDataFetcherError<T>(
|
||||
fn: () => Promise<T>
|
||||
): Promise<DataFetcherResponse<T>> {
|
||||
try {
|
||||
return { data: await fn() };
|
||||
} catch (error) {
|
||||
return {
|
||||
error: getExposableError(error as Error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getExposableError(error: Error): DataFetcherErrorData {
|
||||
if (error instanceof GitBookAPIError) {
|
||||
return {
|
||||
code: error.code,
|
||||
message: error.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof DataFetcherError) {
|
||||
return {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './api';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './pages';
|
||||
export * from './urls';
|
||||
export * from './errors';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import { getDataOrNull } from './errors';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
/**
|
||||
* Get the document for a page.
|
||||
*/
|
||||
export async function getPageDocument(
|
||||
dataFetcher: GitBookDataFetcher,
|
||||
space: Space,
|
||||
page: RevisionPageDocument
|
||||
): Promise<JSONDocument | null> {
|
||||
if (page.documentId) {
|
||||
return getDataOrNull(
|
||||
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
|
||||
);
|
||||
}
|
||||
if (page.computed) {
|
||||
return getDataOrNull(
|
||||
dataFetcher.getComputedDocument({
|
||||
organizationId: space.organization,
|
||||
spaceId: space.id,
|
||||
source: page.computed,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
import type * as api from '@gitbook/api';
|
||||
|
||||
export type DataFetcherErrorData = {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type DataFetcherResponse<T> =
|
||||
| {
|
||||
data: T;
|
||||
error?: undefined;
|
||||
}
|
||||
| {
|
||||
error: DataFetcherErrorData;
|
||||
data?: undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic fetcher for GitBook data.
|
||||
* It is used between v1 and v2.
|
||||
@@ -25,7 +40,7 @@ export interface GitBookDataFetcher {
|
||||
/**
|
||||
* Get a user by its ID.
|
||||
*/
|
||||
getUserById(userId: string): Promise<api.User | null>;
|
||||
getUserById(userId: string): Promise<DataFetcherResponse<api.User>>;
|
||||
|
||||
/**
|
||||
* Get a published content by its URL.
|
||||
@@ -34,7 +49,7 @@ export interface GitBookDataFetcher {
|
||||
url: string;
|
||||
visitorAuthToken: string | null;
|
||||
redirectOnError: boolean;
|
||||
}): Promise<api.PublishedSiteContentLookup>;
|
||||
}): Promise<DataFetcherResponse<api.PublishedSiteContentLookup>>;
|
||||
|
||||
/**
|
||||
* Get a published content site by its organization ID and site ID.
|
||||
@@ -43,12 +58,14 @@ export interface GitBookDataFetcher {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
siteShareKey: string | undefined;
|
||||
}): Promise<api.PublishedContentSite>;
|
||||
}): Promise<DataFetcherResponse<api.PublishedContentSite>>;
|
||||
|
||||
/**
|
||||
* Get a space by its ID.
|
||||
*/
|
||||
getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise<api.Space>;
|
||||
getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise<
|
||||
DataFetcherResponse<api.Space>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Get a change request by its space ID and change request ID.
|
||||
@@ -56,7 +73,7 @@ export interface GitBookDataFetcher {
|
||||
getChangeRequest(params: {
|
||||
spaceId: string;
|
||||
changeRequestId: string;
|
||||
}): Promise<api.ChangeRequest | null>;
|
||||
}): Promise<DataFetcherResponse<api.ChangeRequest>>;
|
||||
|
||||
/**
|
||||
* Get the revision by its space ID and revision ID.
|
||||
@@ -65,7 +82,7 @@ export interface GitBookDataFetcher {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
metadata: boolean;
|
||||
}): Promise<api.Revision>;
|
||||
}): Promise<DataFetcherResponse<api.Revision>>;
|
||||
|
||||
/**
|
||||
* Get the revision pages by its space ID and revision ID.
|
||||
@@ -74,7 +91,7 @@ export interface GitBookDataFetcher {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
metadata: boolean;
|
||||
}): Promise<api.RevisionPage[]>;
|
||||
}): Promise<DataFetcherResponse<api.RevisionPage[]>>;
|
||||
|
||||
/**
|
||||
* Get a revision file by its space ID, revision ID and file ID.
|
||||
@@ -83,7 +100,7 @@ export interface GitBookDataFetcher {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
fileId: string;
|
||||
}): Promise<api.RevisionFile | null>;
|
||||
}): Promise<DataFetcherResponse<api.RevisionFile>>;
|
||||
|
||||
/**
|
||||
* Get a revision page by its path.
|
||||
@@ -92,12 +109,14 @@ export interface GitBookDataFetcher {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
path: string;
|
||||
}): Promise<api.RevisionPageDocument | api.RevisionPageGroup | null>;
|
||||
}): Promise<DataFetcherResponse<api.RevisionPageDocument | api.RevisionPageGroup>>;
|
||||
|
||||
/**
|
||||
* Get a document by its space ID and document ID.
|
||||
*/
|
||||
getDocument(params: { spaceId: string; documentId: string }): Promise<api.JSONDocument>;
|
||||
getDocument(params: { spaceId: string; documentId: string }): Promise<
|
||||
DataFetcherResponse<api.JSONDocument>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Get a computed document by its space ID and computed source.
|
||||
@@ -106,7 +125,7 @@ export interface GitBookDataFetcher {
|
||||
organizationId: string;
|
||||
spaceId: string;
|
||||
source: api.ComputedContentSource;
|
||||
}): Promise<api.JSONDocument>;
|
||||
}): Promise<DataFetcherResponse<api.JSONDocument>>;
|
||||
|
||||
/**
|
||||
* Get a reusable content by its space ID, revision ID and reusable content ID.
|
||||
@@ -115,7 +134,7 @@ export interface GitBookDataFetcher {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
reusableContentId: string;
|
||||
}): Promise<api.RevisionReusableContent | null>;
|
||||
}): Promise<DataFetcherResponse<api.RevisionReusableContent>>;
|
||||
|
||||
/**
|
||||
* Get the latest OpenAPI spec version content by its organization ID and slug.
|
||||
@@ -123,7 +142,7 @@ export interface GitBookDataFetcher {
|
||||
getLatestOpenAPISpecVersionContent(params: {
|
||||
organizationId: string;
|
||||
slug: string;
|
||||
}): Promise<api.OpenAPISpecContent | null>;
|
||||
}): Promise<DataFetcherResponse<api.OpenAPISpecContent>>;
|
||||
|
||||
/**
|
||||
* Get a site redirect by its source path.
|
||||
@@ -133,12 +152,14 @@ export interface GitBookDataFetcher {
|
||||
siteId: string;
|
||||
siteShareKey: string | undefined;
|
||||
source: string;
|
||||
}): Promise<{ redirect: api.SiteRedirect | null; target: string } | null>;
|
||||
}): Promise<DataFetcherResponse<{ redirect: api.SiteRedirect | null; target: string }>>;
|
||||
|
||||
/**
|
||||
* Get an embed by its URL.
|
||||
*/
|
||||
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<api.Embed>;
|
||||
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<
|
||||
DataFetcherResponse<api.Embed>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Search content in a site.
|
||||
@@ -153,5 +174,5 @@ export interface GitBookDataFetcher {
|
||||
| { mode: 'specific'; siteSpaceIds: string[] };
|
||||
/** Cache bust to ensure the search results are fresh when the space is updated. */
|
||||
cacheBust?: string;
|
||||
}): Promise<api.SearchSpaceResult[]>;
|
||||
}): Promise<DataFetcherResponse<api.SearchSpaceResult[]>>;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
/**
|
||||
* Get the document for a page.
|
||||
*/
|
||||
export async function getPageDocument(
|
||||
dataFetcher: GitBookDataFetcher,
|
||||
space: Space,
|
||||
page: RevisionPageDocument
|
||||
) {
|
||||
if (page.documentId) {
|
||||
return dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId });
|
||||
}
|
||||
if (page.computed) {
|
||||
return dataFetcher.getComputedDocument({
|
||||
organizationId: space.organization,
|
||||
spaceId: space.id,
|
||||
source: page.computed,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -33,12 +33,12 @@ export async function middleware(request: NextRequest) {
|
||||
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
|
||||
*/
|
||||
if (extracted.url.pathname.endsWith('/~gitbook/image')) {
|
||||
return serveResizedImage(request, {
|
||||
return await serveResizedImage(request, {
|
||||
host: extracted.url.host,
|
||||
});
|
||||
}
|
||||
|
||||
return serveSiteByURL(request, extracted);
|
||||
return await serveSiteByURL(request, extracted);
|
||||
}
|
||||
|
||||
// Handle the rest with the router default logic
|
||||
@@ -147,10 +147,10 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
|
||||
*/
|
||||
function serveErrorResponse(error: Error) {
|
||||
if (error instanceof GitBookAPIError) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 500, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
return new Response(error.errorMessage, {
|
||||
status: error.code,
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { headers } from 'next/headers';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
|
||||
import { DateRelative } from '../primitives';
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
@@ -107,11 +108,13 @@ async function RevisionToolbar(props: { context: GitBookSiteContext }) {
|
||||
const { context } = props;
|
||||
const { space, revisionId } = context;
|
||||
|
||||
const revision = await context.dataFetcher.getRevision({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
metadata: true,
|
||||
});
|
||||
const revision = await throwIfDataError(
|
||||
context.dataFetcher.getRevision({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
metadata: true,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolbarLayout>
|
||||
|
||||
@@ -5,6 +5,7 @@ import ReactDOM from 'react-dom';
|
||||
import { Card } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import type { BlockProps } from './Block';
|
||||
import { Caption } from './Caption';
|
||||
import { IntegrationBlock } from './Integration';
|
||||
@@ -18,10 +19,16 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
|
||||
ReactDOM.preload('https://cdn.iframe.ly/embed.js', { as: 'script' });
|
||||
|
||||
const embed = await context.contentContext.dataFetcher.getEmbedByUrl({
|
||||
url: block.data.url,
|
||||
spaceId: context.contentContext.space?.id,
|
||||
});
|
||||
const embed = await getDataOrNull(
|
||||
context.contentContext.dataFetcher.getEmbedByUrl({
|
||||
url: block.data.url,
|
||||
spaceId: context.contentContext.space?.id,
|
||||
})
|
||||
);
|
||||
|
||||
if (!embed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Caption {...props} withBorder>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type ContentKitServerContext,
|
||||
} from '@gitbook/react-contentkit';
|
||||
|
||||
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
|
||||
import { renderIntegrationUi } from '@/lib/api';
|
||||
import { parseMarkdown } from '@/lib/markdown';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
|
||||
@@ -14,6 +14,7 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { PlainCodeBlock } from '../CodeBlock';
|
||||
import './contentkit.css';
|
||||
import { ignoreAllThrownError } from '@v2/lib/data';
|
||||
|
||||
const outputContext: ContentKitServerContext = {
|
||||
icons: {
|
||||
@@ -63,9 +64,8 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
context: contentKitContext,
|
||||
};
|
||||
|
||||
const initialOutput = await ignoreAPIError(
|
||||
renderIntegrationUi(block.data.integration, initialInput),
|
||||
true
|
||||
const initialOutput = await ignoreAllThrownError(
|
||||
renderIntegrationUi(block.data.integration, initialInput)
|
||||
);
|
||||
if (!initialOutput || initialOutput.type === 'complete') {
|
||||
return null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { DocumentBlockReusableContent } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import type { BlockProps } from './Block';
|
||||
import { UnwrappedBlocks } from './Blocks';
|
||||
|
||||
@@ -17,10 +18,12 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = await context.contentContext.dataFetcher.getDocument({
|
||||
spaceId: context.contentContext.space.id,
|
||||
documentId: resolved.reusableContent.document,
|
||||
});
|
||||
const document = await getDataOrNull(
|
||||
context.contentContext.dataFetcher.getDocument({
|
||||
spaceId: context.contentContext.space.id,
|
||||
documentId: resolved.reusableContent.document,
|
||||
})
|
||||
);
|
||||
|
||||
if (!document) {
|
||||
return null;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
|
||||
@@ -133,11 +134,13 @@ export async function streamAskQuestion({
|
||||
if (!spacePromises.has(source.space)) {
|
||||
spacePromises.set(
|
||||
source.space,
|
||||
context.dataFetcher.getRevisionPages({
|
||||
spaceId: source.space,
|
||||
revisionId: source.revision,
|
||||
metadata: false,
|
||||
})
|
||||
throwIfDataError(
|
||||
context.dataFetcher.getRevisionPages({
|
||||
spaceId: source.space,
|
||||
revisionId: source.revision,
|
||||
metadata: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -229,17 +232,21 @@ async function searchSiteContent(
|
||||
}
|
||||
|
||||
const [searchResults, { structure }] = await Promise.all([
|
||||
dataFetcher.searchSiteContent({
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
query,
|
||||
scope,
|
||||
}),
|
||||
dataFetcher.getPublishedContentSite({
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
siteShareKey: siteURLData.shareKey,
|
||||
}),
|
||||
throwIfDataError(
|
||||
dataFetcher.searchSiteContent({
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
query,
|
||||
scope,
|
||||
})
|
||||
),
|
||||
throwIfDataError(
|
||||
dataFetcher.getPublishedContentSite({
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
siteShareKey: siteURLData.shareKey,
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { resolvePageId, resolvePagePath } from '@/lib/pages';
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
|
||||
export interface PagePathParams {
|
||||
pathname?: string | string[];
|
||||
@@ -54,22 +55,26 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
|
||||
// If page can't be found, we try with the API, in case we have a redirect at space level.
|
||||
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
|
||||
// The page rendering will take care of redirecting to a normalized pathname.
|
||||
const resolved = await context.dataFetcher.getRevisionPageByPath({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
path: rawPathname,
|
||||
});
|
||||
const resolved = await getDataOrNull(
|
||||
context.dataFetcher.getRevisionPageByPath({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
path: rawPathname,
|
||||
})
|
||||
);
|
||||
if (resolved) {
|
||||
return resolvePageId(pages, resolved.id);
|
||||
}
|
||||
|
||||
// If a page still can't be found, we try with the API, in case we have a redirect at site level.
|
||||
const resolvedSiteRedirect = await context.dataFetcher.getSiteRedirectBySource({
|
||||
organizationId,
|
||||
siteId: site.id,
|
||||
source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`,
|
||||
siteShareKey: shareKey,
|
||||
});
|
||||
const resolvedSiteRedirect = await getDataOrNull(
|
||||
context.dataFetcher.getSiteRedirectBySource({
|
||||
organizationId,
|
||||
siteId: site.id,
|
||||
source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`,
|
||||
siteShareKey: shareKey,
|
||||
})
|
||||
);
|
||||
if (resolvedSiteRedirect) {
|
||||
return redirect(resolvedSiteRedirect.target);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type RevisionFile,
|
||||
type RevisionReusableContent,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookDataFetcher } from '@v2/lib/data/types';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { batch } from './async';
|
||||
@@ -865,50 +864,6 @@ export const getPublishedContentSite = cache({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch all the content data about a space at once.
|
||||
* This function executes the requests in parallel and should be used as early as possible
|
||||
* instead of calling the individual functions.
|
||||
*
|
||||
* @deprecated - use 'fetchSpaceContextByIds' from v2
|
||||
*/
|
||||
export async function getSpaceContentData(
|
||||
dataFetcher: GitBookDataFetcher,
|
||||
pointer: SpaceContentPointer,
|
||||
shareKey: string | undefined
|
||||
) {
|
||||
const [space, changeRequest] = await Promise.all([
|
||||
dataFetcher.getSpace({
|
||||
spaceId: pointer.spaceId,
|
||||
shareKey,
|
||||
}),
|
||||
pointer.changeRequestId
|
||||
? dataFetcher.getChangeRequest({
|
||||
spaceId: pointer.spaceId,
|
||||
changeRequestId: pointer.changeRequestId,
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
|
||||
const contentTarget: ContentTarget = {
|
||||
spaceId: pointer.spaceId,
|
||||
revisionId: changeRequest?.revision ?? pointer.revisionId ?? space.revision,
|
||||
};
|
||||
const pages = await dataFetcher.getRevisionPages({
|
||||
spaceId: pointer.spaceId,
|
||||
revisionId: contentTarget.revisionId,
|
||||
// We only care about the Git metadata when the Git sync is enabled,
|
||||
// otherwise we can optimize performance by not fetching it
|
||||
metadata: !!space.gitSync,
|
||||
});
|
||||
|
||||
return {
|
||||
space,
|
||||
pages,
|
||||
contentTarget,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search content in a Site or specific SiteSpaces.
|
||||
*/
|
||||
@@ -1009,22 +964,6 @@ export function userAgent(): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore error for an API call.
|
||||
*/
|
||||
export async function ignoreAPIError<T>(promise: Promise<T>, ignoreAll = false): Promise<T | null> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (error) {
|
||||
const code = (error as GitBookAPIError).code;
|
||||
if (ignoreAll || (code >= 400 && code < 500)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over a paginated API endpoint and return all the items.
|
||||
*/
|
||||
|
||||
@@ -7,14 +7,13 @@ import type {
|
||||
} from '@gitbook/api';
|
||||
import type { Filesystem } from '@gitbook/openapi-parser';
|
||||
import { type GitBookAnyContext, fetchSpaceContextByIds } from '@v2/lib/context';
|
||||
import { getPageDocument } from '@v2/lib/data';
|
||||
import { getDataOrNull, getPageDocument, ignoreDataThrownError } from '@v2/lib/data';
|
||||
import { createLinker } from '@v2/lib/links';
|
||||
import assertNever from 'assert-never';
|
||||
import type React from 'react';
|
||||
|
||||
import { PageIcon } from '@/components/PageIcon';
|
||||
|
||||
import { ignoreAPIError } from './api';
|
||||
import { getBlockById, getBlockTitle } from './document';
|
||||
import { getGitbookAppHref } from './links';
|
||||
import { resolvePageId } from './pages';
|
||||
@@ -84,11 +83,13 @@ export async function resolveContentRef(
|
||||
}
|
||||
|
||||
case 'file': {
|
||||
const file = await dataFetcher.getRevisionFile({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
fileId: contentRef.file,
|
||||
});
|
||||
const file = await getDataOrNull(
|
||||
dataFetcher.getRevisionFile({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
fileId: contentRef.file,
|
||||
})
|
||||
);
|
||||
if (file) {
|
||||
return {
|
||||
href: file.downloadURL,
|
||||
@@ -189,7 +190,7 @@ export async function resolveContentRef(
|
||||
}
|
||||
|
||||
case 'user': {
|
||||
const user = await dataFetcher.getUserById(contentRef.user);
|
||||
const user = await getDataOrNull(dataFetcher.getUserById(contentRef.user));
|
||||
if (user) {
|
||||
return {
|
||||
href: `mailto:${user.email}`,
|
||||
@@ -209,11 +210,13 @@ export async function resolveContentRef(
|
||||
}
|
||||
|
||||
case 'reusable-content': {
|
||||
const reusableContent = await dataFetcher.getReusableContent({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
reusableContentId: contentRef.reusableContent,
|
||||
});
|
||||
const reusableContent = await getDataOrNull(
|
||||
dataFetcher.getReusableContent({
|
||||
spaceId: space.id,
|
||||
revisionId,
|
||||
reusableContentId: contentRef.reusableContent,
|
||||
})
|
||||
);
|
||||
if (!reusableContent) {
|
||||
return null;
|
||||
}
|
||||
@@ -226,10 +229,12 @@ export async function resolveContentRef(
|
||||
}
|
||||
|
||||
case 'openapi': {
|
||||
const openAPISpecVersionContent = await dataFetcher.getLatestOpenAPISpecVersionContent({
|
||||
organizationId: context.organizationId,
|
||||
slug: contentRef.spec,
|
||||
});
|
||||
const openAPISpecVersionContent = await getDataOrNull(
|
||||
dataFetcher.getLatestOpenAPISpecVersionContent({
|
||||
organizationId: context.organizationId,
|
||||
slug: contentRef.spec,
|
||||
})
|
||||
);
|
||||
|
||||
if (!openAPISpecVersionContent) {
|
||||
return null;
|
||||
@@ -258,19 +263,21 @@ async function getBestTargetSpace(
|
||||
const { dataFetcher } = context;
|
||||
|
||||
const [fetchedSpace, publishedContentSite] = await Promise.all([
|
||||
ignoreAPIError(
|
||||
getDataOrNull(
|
||||
dataFetcher.getSpace({
|
||||
spaceId,
|
||||
shareKey: context?.shareKey,
|
||||
})
|
||||
}),
|
||||
[404, 403]
|
||||
),
|
||||
'site' in context
|
||||
? ignoreAPIError(
|
||||
? getDataOrNull(
|
||||
dataFetcher.getPublishedContentSite({
|
||||
organizationId: context.organizationId,
|
||||
siteId: context.site.id,
|
||||
siteShareKey: context.shareKey,
|
||||
})
|
||||
}),
|
||||
[404, 403]
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
@@ -294,7 +301,7 @@ async function resolveContentRefInSpace(
|
||||
contentRef: ContentRef
|
||||
) {
|
||||
const [spaceContext, bestTargetSpace] = await Promise.all([
|
||||
ignoreAPIError(
|
||||
ignoreDataThrownError(
|
||||
fetchSpaceContextByIds(context, {
|
||||
space: spaceId,
|
||||
shareKey: context?.shareKey,
|
||||
|
||||
+124
-25
@@ -8,6 +8,7 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types';
|
||||
import { createImageResizer } from '@v2/lib/images';
|
||||
import { createLinker } from '@v2/lib/links';
|
||||
|
||||
import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data';
|
||||
import {
|
||||
type SiteContentPointer,
|
||||
type SpaceContentPointer,
|
||||
@@ -89,78 +90,176 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
|
||||
},
|
||||
|
||||
getUserById(userId) {
|
||||
return getUserById(userId);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const user = await getUserById(userId);
|
||||
if (!user) {
|
||||
throw new DataFetcherError('User not found', 404);
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
},
|
||||
|
||||
// @ts-ignore - types are compatible enough, and this will not be called in v1 this way
|
||||
getPublishedContentByUrl(params) {
|
||||
return getPublishedContentByUrl(
|
||||
params.url,
|
||||
params.visitorAuthToken ?? undefined,
|
||||
params.redirectOnError ? true : undefined
|
||||
);
|
||||
return wrapDataFetcherError(async () => {
|
||||
return getPublishedContentByUrl(
|
||||
params.url,
|
||||
params.visitorAuthToken ?? undefined,
|
||||
params.redirectOnError ? true : undefined
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
getPublishedContentSite(params) {
|
||||
return getPublishedContentSite(params);
|
||||
return wrapDataFetcherError(async () => {
|
||||
return getPublishedContentSite(params);
|
||||
});
|
||||
},
|
||||
|
||||
getSpace(params) {
|
||||
return getSpace(params.spaceId, params.shareKey);
|
||||
return wrapDataFetcherError(async () => {
|
||||
return getSpace(params.spaceId, params.shareKey);
|
||||
});
|
||||
},
|
||||
|
||||
getChangeRequest(params) {
|
||||
return getChangeRequest(params.spaceId, params.changeRequestId);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const changeRequest = await getChangeRequest(
|
||||
params.spaceId,
|
||||
params.changeRequestId
|
||||
);
|
||||
if (!changeRequest) {
|
||||
throw new DataFetcherError('Change request not found', 404);
|
||||
}
|
||||
|
||||
return changeRequest;
|
||||
});
|
||||
},
|
||||
|
||||
getRevision(params) {
|
||||
return getRevision(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
return wrapDataFetcherError(async () => {
|
||||
return getRevision(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getRevisionFile(params) {
|
||||
return getRevisionFile(params.spaceId, params.revisionId, params.fileId);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const revisionFile = await getRevisionFile(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.fileId
|
||||
);
|
||||
if (!revisionFile) {
|
||||
throw new DataFetcherError('Revision file not found', 404);
|
||||
}
|
||||
|
||||
return revisionFile;
|
||||
});
|
||||
},
|
||||
|
||||
getDocument(params) {
|
||||
return getDocument(params.spaceId, params.documentId);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const document = await getDocument(params.spaceId, params.documentId);
|
||||
if (!document) {
|
||||
throw new DataFetcherError('Document not found', 404);
|
||||
}
|
||||
|
||||
return document;
|
||||
});
|
||||
},
|
||||
|
||||
getComputedDocument(params) {
|
||||
return getComputedDocument(params.organizationId, params.spaceId, params.source);
|
||||
return wrapDataFetcherError(() => {
|
||||
return getComputedDocument(params.organizationId, params.spaceId, params.source);
|
||||
});
|
||||
},
|
||||
|
||||
getRevisionPages(params) {
|
||||
return getRevisionPages(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
return wrapDataFetcherError(async () => {
|
||||
return getRevisionPages(params.spaceId, params.revisionId, {
|
||||
metadata: params.metadata,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getRevisionPageByPath(params) {
|
||||
return getRevisionPageByPath(params.spaceId, params.revisionId, params.path);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const revisionPage = await getRevisionPageByPath(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.path
|
||||
);
|
||||
|
||||
if (!revisionPage) {
|
||||
throw new DataFetcherError('Revision page not found', 404);
|
||||
}
|
||||
|
||||
return revisionPage;
|
||||
});
|
||||
},
|
||||
|
||||
getReusableContent(params) {
|
||||
return getReusableContent(params.spaceId, params.revisionId, params.reusableContentId);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const reusableContent = await getReusableContent(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
params.reusableContentId
|
||||
);
|
||||
|
||||
if (!reusableContent) {
|
||||
throw new DataFetcherError('Reusable content not found', 404);
|
||||
}
|
||||
|
||||
return reusableContent;
|
||||
});
|
||||
},
|
||||
|
||||
getLatestOpenAPISpecVersionContent(params) {
|
||||
return getLatestOpenAPISpecVersionContent(params.organizationId, params.slug);
|
||||
return wrapDataFetcherError(async () => {
|
||||
const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
|
||||
params.organizationId,
|
||||
params.slug
|
||||
);
|
||||
|
||||
if (!openAPISpecVersionContent) {
|
||||
throw new DataFetcherError('OpenAPI spec version content not found', 404);
|
||||
}
|
||||
|
||||
return openAPISpecVersionContent;
|
||||
});
|
||||
},
|
||||
|
||||
getSiteRedirectBySource(params) {
|
||||
return getSiteRedirectBySource(params);
|
||||
},
|
||||
return wrapDataFetcherError(async () => {
|
||||
const siteRedirect = await getSiteRedirectBySource(params);
|
||||
if (!siteRedirect) {
|
||||
throw new DataFetcherError('Site redirect not found', 404);
|
||||
}
|
||||
|
||||
return siteRedirect;
|
||||
});
|
||||
},
|
||||
getEmbedByUrl(params) {
|
||||
return getEmbedByUrlInSpace(params.spaceId, params.url);
|
||||
return wrapDataFetcherError(() => {
|
||||
return getEmbedByUrlInSpace(params.spaceId, params.url);
|
||||
});
|
||||
},
|
||||
|
||||
async searchSiteContent(params) {
|
||||
const { organizationId, siteId, query, cacheBust, scope } = params;
|
||||
const result = await searchSiteContent(organizationId, siteId, query, scope, cacheBust);
|
||||
return result.items;
|
||||
return wrapDataFetcherError(async () => {
|
||||
const { organizationId, siteId, query, cacheBust, scope } = params;
|
||||
const result = await searchSiteContent(
|
||||
organizationId,
|
||||
siteId,
|
||||
query,
|
||||
scope,
|
||||
cacheBust
|
||||
);
|
||||
return result.items;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { joinPath } from '@/lib/paths';
|
||||
import { getIndexablePages } from '@/lib/sitemap';
|
||||
import { getSiteStructureSections } from '@/lib/sites';
|
||||
import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
|
||||
/**
|
||||
* Generate a llms.txt file for the site.
|
||||
@@ -106,11 +107,13 @@ async function getNodesFromSiteSpaces(
|
||||
if (!siteSpaceUrl) {
|
||||
return [];
|
||||
}
|
||||
const rootPages = await dataFetcher.getRevisionPages({
|
||||
spaceId: siteSpace.space.id,
|
||||
revisionId: siteSpace.space.revision,
|
||||
metadata: false,
|
||||
});
|
||||
const rootPages = await throwIfDataError(
|
||||
dataFetcher.getRevisionPages({
|
||||
spaceId: siteSpace.space.id,
|
||||
revisionId: siteSpace.space.revision,
|
||||
metadata: false,
|
||||
})
|
||||
);
|
||||
const pages = getIndexablePages(rootPages);
|
||||
const listChildren = await Promise.all(
|
||||
pages.map(async ({ page }): Promise<ListItem> => {
|
||||
|
||||
Reference in New Issue
Block a user