Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: FDR Latest pulling at the edge (S3 upload/download) #1994

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions fern/apis/fdr/definition/api/latest/__package__.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ types:
auths: map<auth.AuthSchemeId, auth.AuthScheme>
globalHeaders: optional<list<type.ObjectProperty>>

LatestApiDefinition:
discriminated: false
union:
- string
- ApiDefinition

errors:
ApiDoesNotExistError:
status-code: 404
2 changes: 1 addition & 1 deletion fern/apis/fdr/definition/docs/v1/read/__package__.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ types:
algoliaSearchIndex: optional<algolia.AlgoliaSearchIndex>
pages: map<rootCommons.PageId, PageContent>
apis: map<rootCommons.ApiDefinitionId, apiReadV1.ApiDefinition>
apisV2: map<rootCommons.ApiDefinitionId, apiReadLatest.ApiDefinition>
apisV2: map<rootCommons.ApiDefinitionId, apiReadLatest.LatestApiDefinition>
files: map<rootCommons.FileId, rootCommons.Url>
filesV2: map<rootCommons.FileId, File>
jsFiles:
Expand Down
2 changes: 2 additions & 0 deletions packages/fdr-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"test": "vitest --run"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.726.1",
"@aws-sdk/s3-request-presigner": "^3.726.1",
"@fern-api/ui-core-utils": "workspace:*",
"@ungap/structured-clone": "^1.2.0",
"dayjs": "^1.11.11",
Expand Down
1 change: 1 addition & 0 deletions packages/fdr-sdk/src/api-definition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./lang";
export * from "./latest";
export * from "./migrators/v1ToV2";
export * from "./prune";
export * from "./s3Loader";
export * from "./snippets/curl";
export * from "./snippets/SnippetHttpRequest";
export * from "./status-message";
Expand Down
51 changes: 51 additions & 0 deletions packages/fdr-sdk/src/api-definition/s3Loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { FdrAPI } from "../client";
import { LatestApiDefinition } from "./latest";

export class S3Loader {
private s3Client: S3Client | undefined;
private bucketName: string | undefined;

constructor() {
if (
process.env.AWS_ACCESS_KEY_ID != null &&
process.env.AWS_SECRET_ACCESS_KEY != null &&
process.env.AWS_REGION != null &&
process.env.AWS_S3_BUCKET_NAME != null
) {
this.s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
this.bucketName = process.env.AWS_S3_BUCKET_NAME;
}
}

async loadApiDefinition(
apiDefinitionOrKey: LatestApiDefinition
): Promise<FdrAPI.api.latest.ApiDefinition> {
if (this.s3Client == null) {
throw new Error("S3 client not initialized");
}

let resolvedApi: FdrAPI.api.latest.ApiDefinition;
if (typeof apiDefinitionOrKey === "string") {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: apiDefinitionOrKey,
});
const url = await getSignedUrl(this.s3Client, command, {
expiresIn: 604800,
});
resolvedApi = await (await fetch(url)).json();
} else {
resolvedApi = apiDefinitionOrKey;
}

return resolvedApi;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mapValues } from "es-toolkit/object";
import { APIV1Db, APIV1Read, DocsV1Db, DocsV1Read } from "../../client";
import { APIV1Db, APIV1Read, DocsV1Db, DocsV1Read, FdrAPI } from "../../client";
import { SearchInfo } from "../../client/FdrAPI";
import { FernRegistry } from "../../client/generated";
import { convertDbDocsConfigToRead } from "./convertDbDocsConfigToRead";
Expand All @@ -19,7 +19,7 @@ export function convertDocsDefinitionToRead({
apis: Record<DocsV1Db.ApiDefinitionId, APIV1Read.ApiDefinition>;
apisV2: Record<
FernRegistry.ApiDefinitionId,
FernRegistry.api.latest.ApiDefinition
FdrAPI.api.latest.LatestApiDefinition
>;
id: APIV1Db.DocsConfigId | undefined;
search: SearchInfo;
Expand Down
29 changes: 27 additions & 2 deletions packages/fdr-sdk/src/navigation/utils/toApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import { mapValues } from "es-toolkit/object";
import { ApiDefinition } from "../..";
import { DocsV2Read } from "../../client";

export function toApis(docs: DocsV2Read.LoadDocsForUrlResponse) {
export async function toApis(
docs: DocsV2Read.LoadDocsForUrlResponse,
getPresignedDocsAssetsDownloadUrl: ({
key,
isPrivate,
}: {
key: string;
isPrivate: boolean;
}) => Promise<string>
) {
return {
...mapValues(docs.definition.apis, (api) =>
ApiDefinition.ApiDefinitionV1ToLatest.from(api, {
Expand All @@ -11,6 +20,22 @@ export function toApis(docs: DocsV2Read.LoadDocsForUrlResponse) {
usesApplicationJsonInFormDataValue: false,
}).migrate()
),
...docs.definition.apisV2,
...Object.fromEntries(
await Promise.all(
Object.entries(docs.definition.apisV2 ?? {}).map(async ([key, def]) => {
if (typeof def === "string") {
const url = await getPresignedDocsAssetsDownloadUrl({
key: def,
isPrivate: true,
});
const response = await fetch(url.toString());
const apiDefinition = await response.json();
return [key, apiDefinition as ApiDefinition.ApiDefinition];
} else {
return [key, def];
}
})
)
),
};
}
11 changes: 7 additions & 4 deletions packages/fern-docs/bundle/src/server/DocsLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { DocsV1Read, DocsV2Read } from "@fern-api/fdr-sdk";
import {
ApiDefinition,
ApiDefinitionV1ToLatest,
S3Loader,
} from "@fern-api/fdr-sdk/api-definition";
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import type { AuthEdgeConfig } from "@fern-docs/auth";
Expand Down Expand Up @@ -103,11 +104,13 @@ export class DocsLoader {
return undefined;
}
const v1 = res.definition.apis[key];
const s3Loader = new S3Loader();
const latest =
res.definition.apisV2?.[key] ??
(v1 != null
? ApiDefinitionV1ToLatest.from(v1, this.edgeFlags).migrate()
: undefined);
res.definition.apisV2?.[key] != null
? await s3Loader.loadApiDefinition(res.definition.apisV2[key])
: v1 != null
? ApiDefinitionV1ToLatest.from(v1, this.edgeFlags).migrate()
: undefined;
if (!latest) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,26 @@ for (const fixtureName of [
describe(fixtureName, () => {
it("should work", async () => {
const fixture = readFixture(fixtureName);
const mockGetPresignedDocsAssetsDownloadUrl = vi.fn();
const root = FernNavigation.utils.toRootNode(fixture);
const apis = FernNavigation.utils.toApis(fixture);
console.log(`${fixtureName} - a`);
const apis = await FernNavigation.utils.toApis(
fixture,
mockGetPresignedDocsAssetsDownloadUrl
);
console.log(`${fixtureName} - b`);
const pages = FernNavigation.utils.toPages(fixture);

console.log(`${fixtureName} - c`);
const { records, tooLarge } = createAlgoliaRecords({
root,
domain: "test.com",
org_id: "test",
pages,
apis,
});

console.log(`${fixtureName} - d`);
expect(tooLarge.length).toBe(0);

console.log(`${fixtureName} - e`);
records.forEach((record) => {
if (record.description != null) {
expect(record.description.length).toBeLessThanOrEqual(50_000);
Expand All @@ -73,9 +79,11 @@ for (const fixtureName of [
expect(record.content.length).toBeLessThanOrEqual(50_000);
}
});
console.log(`${fixtureName} - f`);
await expect(JSON.stringify(records, null, 2)).toMatchFileSnapshot(
path.join("__snapshots__", `${fixtureName}.test.ts.json`)
);
});
console.log(`${fixtureName} - g`);
}, 100_000);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export function readFixtureToRootNode(
];
})
),
...fixture.definition.apisV2,
...(Object.fromEntries(
Object.entries(fixture.definition.apisV2 ?? {}).filter(
([_, api]) => typeof api !== "string"
)
) as Record<string, ApiDefinition.ApiDefinition>),
};
const pages = mapValues(fixture.definition.pages, (page) => page.markdown);
return { root, apis, pages };
Expand Down
13 changes: 12 additions & 1 deletion packages/fern-docs/search-server/src/fdr/load-docs-with-url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApiDefinition, FdrClient, FernNavigation } from "@fern-api/fdr-sdk";
import { S3Loader } from "@fern-api/fdr-sdk/api-definition";
import { withDefaultProtocol } from "@fern-api/ui-core-utils";
import { mapValues } from "es-toolkit/object";

Expand Down Expand Up @@ -72,6 +73,7 @@ export async function loadDocsWithUrl(
// migrate pages
const pages = mapValues(docs.body.definition.pages, (page) => page.markdown);

const s3Loader = new S3Loader();
// migrate apis
const apis = {
...mapValues(docs.body.definition.apis, (api) =>
Expand All @@ -83,7 +85,16 @@ export async function loadDocsWithUrl(
payload.usesApplicationJsonInFormDataValue ?? false,
}).migrate()
),
...docs.body.definition.apisV2,
...Object.fromEntries(
await Promise.all(
Object.entries(docs.body.definition.apisV2 ?? {}).map(
async ([apiId, api]) => {
const resolvedApi = await s3Loader.loadApiDefinition(api);
return [apiId, resolvedApi];
}
)
)
),
};

return { org_id: org.body, root, pages, apis, domain: domain.host };
Expand Down
32 changes: 22 additions & 10 deletions packages/fern-docs/ui/src/resolver/resolveDocsContent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { ApiDefinitionV1ToLatest } from "@fern-api/fdr-sdk/api-definition";
import {
ApiDefinitionV1ToLatest,
S3Loader,
} from "@fern-api/fdr-sdk/api-definition";
import type {
APIV1Read,
DocsV1Read,
Expand Down Expand Up @@ -31,7 +34,7 @@ interface ResolveDocsContentArgs {
prev: FernNavigation.NavigationNodeNeighbor | undefined;
next: FernNavigation.NavigationNodeNeighbor | undefined;
apis: Record<string, APIV1Read.ApiDefinition>;
apisV2: Record<string, FdrAPI.api.latest.ApiDefinition>;
apisV2: Record<string, FdrAPI.api.latest.LatestApiDefinition>;
pages: Record<string, DocsV1Read.PageContent>;
mdxOptions?: FernSerializeMdxOptions;
edgeFlags: EdgeFlags;
Expand Down Expand Up @@ -69,6 +72,7 @@ export async function resolveDocsContent({
engine
);

const s3Loader = new S3Loader();
// TODO: remove legacy when done
const apiLoaders = {
...mapValues(apis, (api) => {
Expand All @@ -81,14 +85,22 @@ export async function resolveDocsContent({
.withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN)
.withResolveDescriptions();
}),
...mapValues(apisV2 ?? {}, (api) => {
return ApiDefinitionLoader.create(domain, api.id)
.withMdxBundler(serializeMdx, engine)
.withEdgeFlags(edgeFlags)
.withApiDefinition(api)
.withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN)
.withResolveDescriptions();
}),
...Object.fromEntries(
await Promise.all(
Object.entries(apisV2 ?? {}).map(async ([apiId, api]) => {
const resolvedApi = await s3Loader.loadApiDefinition(api);
return [
apiId,
ApiDefinitionLoader.create(domain, resolvedApi.id)
.withMdxBundler(serializeMdx, engine)
.withEdgeFlags(edgeFlags)
.withApiDefinition(resolvedApi)
.withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN)
.withResolveDescriptions(),
];
})
)
),
};

let result: DocsContent | undefined;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading