diff --git a/packages/fern-docs/bundle/src/server/DocsLoader.ts b/packages/fern-docs/bundle/src/server/DocsLoader.ts index 8dd91b034e..dac3e8b8b2 100644 --- a/packages/fern-docs/bundle/src/server/DocsLoader.ts +++ b/packages/fern-docs/bundle/src/server/DocsLoader.ts @@ -123,12 +123,27 @@ export class DocsLoader { DocsV2Read.LoadDocsForUrlResponse | undefined > { if (!this.#loadForDocsUrlResponse) { - const response = await loadWithUrl(this.domain); - - if (response.ok) { - this.#loadForDocsUrlResponse = response.body; - } else { - this.#error = response.error; + try { + const environmentType = process.env.NODE_ENV ?? "development"; + let dbDocsDefUrl = ""; + if (environmentType === "development") { + dbDocsDefUrl = `https://docs-definitions-dev2.buildwithfern.com/${this.domain}.json`; + } else if (environmentType === "production") { + dbDocsDefUrl = `https://docs-definitions.buildwithfern.com/${this.domain}.json`; + } + const response = await fetch(dbDocsDefUrl); + if (response.ok) { + const json = await response.json(); + return json as DocsV2Read.LoadDocsForUrlResponse; + } + } catch { + // Not served by cloudfront, fetch from Redis and then RDS + const response = await loadWithUrl(this.domain); + if (response.ok) { + this.#loadForDocsUrlResponse = response.body; + } else { + this.#error = response.error; + } } } return this.#loadForDocsUrlResponse; diff --git a/servers/fdr-deploy/scripts/fdr-deploy-stack.ts b/servers/fdr-deploy/scripts/fdr-deploy-stack.ts index 0fd3e1f965..95aa266aa1 100644 --- a/servers/fdr-deploy/scripts/fdr-deploy-stack.ts +++ b/servers/fdr-deploy/scripts/fdr-deploy-stack.ts @@ -207,6 +207,53 @@ export class FdrDeployStack extends Stack { } ); + // for revalidate-all and finish-register workflow + const dbDocsDefinitionBucket = new Bucket( + this, + "fdr-docs-definitions-public", + { + bucketName: `fdr-${environmentType.toLowerCase()}-docs-definitions-public`, + cors: [ + { + allowedMethods: [ + HttpMethods.GET, + HttpMethods.POST, + HttpMethods.PUT, + ], + allowedOrigins: ["*"], + allowedHeaders: ["*"], + }, + ], + blockPublicAccess: { + blockPublicAcls: false, + blockPublicPolicy: false, + ignorePublicAcls: false, + restrictPublicBuckets: false, + }, + versioned: true, + } + ); + dbDocsDefinitionBucket.grantPublicAccess(); + + const dbDocsDefinitionDomainName = + environmentType === "PROD" + ? "docs-definitions.buildwithfern.com" + : "docs-definitions-dev2.buildwithfern.com"; + const dbDocsDefinitionDistribution = new cloudfront.Distribution( + this, + "DbDocsDefinitionDistribution", + { + defaultBehavior: { + origin: new origins.S3Origin(dbDocsDefinitionBucket), + viewerProtocolPolicy: + cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, + }, + domainNames: [dbDocsDefinitionDomainName], + certificate, + } + ); + new route53.ARecord(this, "PublicDocsFilesRecord", { recordName: publicDocsFilesDomainName, target: route53.RecordTarget.fromAlias( @@ -215,6 +262,14 @@ export class FdrDeployStack extends Stack { zone: hostedZone, }); + new route53.ARecord(this, "DbDocsDefinitionRecord", { + recordName: dbDocsDefinitionDomainName, + target: route53.RecordTarget.fromAlias( + new targets.CloudFrontTarget(dbDocsDefinitionDistribution) + ), + zone: hostedZone, + }); + const fernDocsCacheEndpoint = this.constructElastiCacheInstance(this, { cacheName: options.cacheName, IVpc: vpc, @@ -265,6 +320,9 @@ export class FdrDeployStack extends Stack { PUBLIC_S3_BUCKET_REGION: publicDocsBucket.stack.region, PRIVATE_S3_BUCKET_NAME: privateDocsBucket.bucketName, PRIVATE_S3_BUCKET_REGION: privateDocsBucket.stack.region, + DB_DOCS_DEFINITION_BUCKET_NAME: dbDocsDefinitionBucket.bucketName, + DB_DOCS_DEFINITION_BUCKET_REGION: + dbDocsDefinitionBucket.stack.region, API_DEFINITION_SOURCE_BUCKET_NAME: privateApiDefinitionSourceBucket.bucketName, API_DEFINITION_SOURCE_BUCKET_REGION: diff --git a/servers/fdr/src/__test__/local/s3.test.ts b/servers/fdr/src/__test__/local/s3.test.ts index 70d985e486..c9bc3d6b06 100644 --- a/servers/fdr/src/__test__/local/s3.test.ts +++ b/servers/fdr/src/__test__/local/s3.test.ts @@ -19,6 +19,11 @@ describe("S3 Service", () => { bucketRegion: "us-east-1", urlOverride: undefined, }, + dbDocsDefinitionS3: { + bucketName: "fdr-dev2-db-docs-def-public", + bucketRegion: "us-east-1", + urlOverride: undefined, + }, privateApiDefinitionSourceS3: { bucketName: "fdr-source-files", bucketRegion: "us-east-1", diff --git a/servers/fdr/src/__test__/mock.ts b/servers/fdr/src/__test__/mock.ts index 866e685d3a..ae68844b22 100644 --- a/servers/fdr/src/__test__/mock.ts +++ b/servers/fdr/src/__test__/mock.ts @@ -150,6 +150,11 @@ export const baseMockFdrConfig: FdrConfig = { bucketRegion: "us-east-1", urlOverride: "http://s3-mock:9090", }, + dbDocsDefinitionS3: { + bucketName: "fdr", + bucketRegion: "us-east-1", + urlOverride: "http://s3-mock:9090", + }, privateApiDefinitionSourceS3: { bucketName: "fdr", bucketRegion: "us-east-1", diff --git a/servers/fdr/src/app/FdrConfig.ts b/servers/fdr/src/app/FdrConfig.ts index c083cf325e..60d2401654 100644 --- a/servers/fdr/src/app/FdrConfig.ts +++ b/servers/fdr/src/app/FdrConfig.ts @@ -10,6 +10,12 @@ const PRIVATE_S3_BUCKET_NAME_ENV_VAR = "PRIVATE_S3_BUCKET_NAME"; const PRIVATE_S3_BUCKET_REGION_ENV_VAR = "PRIVATE_S3_BUCKET_REGION"; const PRIVATE_S3_URL_OVERRIDE_ENV_VAR = "PRIVATE_S3_URL_OVERRIDE"; +const DB_DOCS_DEFINITION_BUCKET_NAME_ENV_VAR = "DB_DOCS_DEFINITION_BUCKET_NAME"; +const DB_DOCS_DEFINITION_BUCKET_REGION_ENV_VAR = + "DB_DOCS_DEFINITION_BUCKET_REGION"; +const DB_DOCS_DEFINITION_BUCKET_URL_OVERRIDE_ENV_VAR = + "DB_DOCS_DEFINITION_BUCKET_URL_OVERRIDE"; + const API_DEFINITION_SOURCE_BUCKET_NAME_ENV_VAR = "API_DEFINITION_SOURCE_BUCKET_NAME"; const API_DEFINITION_SOURCE_BUCKET_REGION_ENV_VAR = @@ -45,6 +51,7 @@ export interface FdrConfig { cdnPublicDocsUrl: string; publicDocsS3: S3Config; privateDocsS3: S3Config; + dbDocsDefinitionS3: S3Config; privateApiDefinitionSourceS3: S3Config; domainSuffix: string; algoliaAppId: string; @@ -80,6 +87,15 @@ export function getConfig(): FdrConfig { ), urlOverride: process.env[PRIVATE_S3_URL_OVERRIDE_ENV_VAR], }, + dbDocsDefinitionS3: { + bucketName: getEnvironmentVariableOrThrow( + DB_DOCS_DEFINITION_BUCKET_NAME_ENV_VAR + ), + bucketRegion: getEnvironmentVariableOrThrow( + DB_DOCS_DEFINITION_BUCKET_REGION_ENV_VAR + ), + urlOverride: process.env[DB_DOCS_DEFINITION_BUCKET_URL_OVERRIDE_ENV_VAR], + }, privateApiDefinitionSourceS3: { bucketName: getEnvironmentVariableOrThrow( API_DEFINITION_SOURCE_BUCKET_NAME_ENV_VAR diff --git a/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts b/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts index a24a8addd8..c3d4136fda 100644 --- a/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts +++ b/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts @@ -277,6 +277,36 @@ export function getDocsWriteV2Service(app: FdrApplication): DocsV2WriteService { indexSegments, }); + const readDocsDefinition = convertDocsDefinitionToRead({ + docsDbDefinition: dbDocsDefinition, + algoliaSearchIndex: undefined, + filesV2: {}, + apis: mapValues(apiDefinitionsById, (def) => + convertDbAPIDefinitionToRead(def) + ), + apisV2: mapValues(apiDefinitionsLatestById, (def) => def), + id: DocsV1Write.DocsConfigId(""), + search: getSearchInfoFromDocs({ + algoliaIndex: undefined, + indexSegmentIds: [], + activeIndexSegments: [], + docsDbDefinition: dbDocsDefinition, + app, + }), + }); + + try { + await app.services.s3.writeDBDocsDefinition({ + domain: docsRegistrationInfo.fernUrl.getFullUrl(), + readDocsDefinition, + }); + } catch (e) { + app.logger.error( + `Error while trying to write DB docs definition for ${docsRegistrationInfo.fernUrl}`, + e + ); + } + /** * IMPORTANT NOTE: * vercel cache is not shared between custom domains, so we need to revalidate on EACH custom domain individually diff --git a/servers/fdr/src/services/s3/S3Service.ts b/servers/fdr/src/services/s3/S3Service.ts index 5d6015645b..474f85ecf7 100644 --- a/servers/fdr/src/services/s3/S3Service.ts +++ b/servers/fdr/src/services/s3/S3Service.ts @@ -2,6 +2,7 @@ import { GetObjectCommand, PutObjectCommand, PutObjectCommandInput, + PutObjectCommandOutput, S3Client, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; @@ -37,6 +38,10 @@ export interface S3ApiDefinitionSourceFileInfo { } export interface S3Service { + writeDBDocsDefinition(arg0: { + domain: string; + readDocsDefinition: any; + }): Promise; getPresignedDocsAssetsUploadUrls({ domain, filepaths, @@ -79,6 +84,7 @@ export class S3ServiceImpl implements S3Service { private publicDocsS3: S3Client; private privateDocsS3: S3Client; private privateApiDefinitionSourceS3: S3Client; + private dbDocsDefinitionS3: S3Client; private presignedDownloadUrlCache = new Cache( 10_000, ONE_WEEK_IN_SECONDS @@ -106,6 +112,16 @@ export class S3ServiceImpl implements S3Service { secretAccessKey: config.awsSecretKey, }, }); + this.dbDocsDefinitionS3 = new S3Client({ + ...(config.dbDocsDefinitionS3.urlOverride != null + ? { endpoint: config.dbDocsDefinitionS3.urlOverride } + : {}), + region: config.dbDocsDefinitionS3.bucketRegion, + credentials: { + accessKeyId: config.awsAccessKey, + secretAccessKey: config.awsSecretKey, + }, + }); this.privateApiDefinitionSourceS3 = new S3Client({ ...(config.privateApiDefinitionSourceS3.urlOverride != null ? { endpoint: config.privateApiDefinitionSourceS3.urlOverride } @@ -315,6 +331,21 @@ export class S3ServiceImpl implements S3Service { }; } + async writeDBDocsDefinition({ + domain, + readDocsDefinition, + }: { + domain: string; + readDocsDefinition: any; + }): Promise { + const command = new PutObjectCommand({ + Bucket: this.config.dbDocsDefinitionS3.bucketName, + Key: `${domain}.json`, + Body: JSON.stringify(readDocsDefinition), + }); + return await this.dbDocsDefinitionS3.send(command); + } + constructS3DocsKey({ domain, time,