From cc8c89671e1d1626f974f784243a0048ce4ace38 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 19 Dec 2024 20:36:37 -0500 Subject: [PATCH] fix --- .prettierrc.json | 3 +- eslint.config.mjs | 2 + packages/fdr-sdk/package.json | 1 - .../fern-docs/bundle/__mocks__/fileMock.js | 1 - .../fern-docs/bundle/__mocks__/styleMock.js | 1 - .../fern-docs/bundle/tsconfig.eslint.json | 3 +- packages/fern-docs/bundle/tsconfig.json | 2 +- .../fern-docs/components/src/FernButtonV2.tsx | 5 +- packages/fern-docs/icons-cdn/tsconfig.json | 2 +- .../desktop/desktop-search-button.tsx | 4 +- .../search-ui/src/components/ui/sheet.tsx | 10 +- .../fern-docs/ui/src/__mocks__/fileMock.js | 1 - .../fern-docs/ui/src/__mocks__/styleMock.js | 1 - .../api-reference/endpoints/EndpointUrl.tsx | 2 +- .../ui/src/mdx/components/card/Card.tsx | 2 +- .../ui/src/mdx/components/code/CodeGroup.tsx | 2 +- .../template-resolver/src/formatSnippet.ts | 2 +- pnpm-lock.yaml | 52 - servers/fdr/.prettierrc.json | 15 - servers/fdr/package.json | 56 +- servers/fdr/src/Cache.ts | 142 +- servers/fdr/src/__test__/ete/ete.test.ts | 30 +- .../src/__test__/ete/setupDockerizedFdr.ts | 32 +- servers/fdr/src/__test__/ete/vitest.config.ts | 8 +- .../fdr/src/__test__/local/algolia.test.ts | 119 +- .../__test__/local/db/cliVersionsDao.test.ts | 772 ++-- .../__test__/local/db/generatorDao.test.ts | 414 +- .../local/db/generatorVersionsDao.test.ts | 1360 +++---- .../fdr/src/__test__/local/db/gitDao.test.ts | 272 +- .../src/__test__/local/db/snippetsDao.test.ts | 1088 +++--- .../fdr/src/__test__/local/revalidate.test.ts | 14 +- servers/fdr/src/__test__/local/s3.test.ts | 124 +- .../src/__test__/local/services/api.test.ts | 125 +- .../src/__test__/local/services/diff.test.ts | 85 +- .../src/__test__/local/services/docs.test.ts | 463 +-- .../__test__/local/services/docsCache.test.ts | 102 +- .../src/__test__/local/services/git.test.ts | 40 +- .../__test__/local/services/ownership.test.ts | 58 +- .../local/services/snippetTemplates.test.ts | 272 +- .../__test__/local/services/snippets.test.ts | 1443 +++---- .../services/snippetsByEndpointId.test.ts | 295 +- .../fdr/src/__test__/local/setupMockFdr.ts | 190 +- servers/fdr/src/__test__/local/util.ts | 186 +- .../fdr/src/__test__/local/vitest.config.ts | 12 +- servers/fdr/src/__test__/mock.ts | 321 +- servers/fdr/src/__test__/octo.ts | 1229 +++--- .../fdr/src/__test__/unit-tests/Cache.test.ts | 78 +- .../__test__/unit-tests/ParsedBaseUrl.test.ts | 52 +- .../src/__test__/unit-tests/algolia.test.ts | 638 +-- ...enerateAlgoliaSearchRecordsForDocs.test.ts | 337 +- .../unit-tests/noncifySemanticVersion.test.ts | 16 +- .../removeVersionFromFullSlug.test.ts | 32 +- .../testTransformApiDefinitionToDb.test.ts | 85 +- .../transformApiDefinitionToDb.test.ts | 145 +- .../transformEndpointEndpointCall.test.ts | 244 +- .../src/__test__/unit-tests/truncate.test.ts | 12 +- servers/fdr/src/app/FdrApplication.ts | 180 +- servers/fdr/src/app/FdrConfig.ts | 158 +- servers/fdr/src/background.ts | 34 +- .../src/controllers/api/getApiReadService.ts | 62 +- .../controllers/api/getRegisterApiService.ts | 501 +-- .../src/controllers/diff/getApiDiffService.ts | 357 +- .../docs-cache/getDocsCacheService.ts | 14 +- .../controllers/docs/v1/getDocsReadService.ts | 476 +-- .../docs/v1/getDocsWriteService.ts | 139 +- .../docs/v2/getDocsReadV2Service.ts | 310 +- .../docs/v2/getDocsWriteV2Service.ts | 870 +++-- .../generators/getGeneratorsCliController.ts | 110 +- .../generators/getGeneratorsRootController.ts | 50 +- .../getGeneratorsVersionsController.ts | 107 +- .../src/controllers/git/getGitController.ts | 173 +- .../sdk/__test__/getLatestVersion.test.ts | 21 +- .../src/controllers/sdk/getLatestVersion.ts | 24 +- .../src/controllers/sdk/getVersionsService.ts | 117 +- .../src/controllers/snippets/APIResolver.ts | 158 +- .../fdr/src/controllers/snippets/AuthUtils.ts | 65 +- .../snippets/getSnippetsFactoryService.ts | 26 +- .../snippets/getSnippetsService.ts | 237 +- .../snippets/getTemplatesService.ts | 188 +- .../controllers/tokens/getTokensService.ts | 54 +- servers/fdr/src/db/FdrDao.ts | 139 +- servers/fdr/src/db/api/APIDefinitionDao.ts | 107 +- servers/fdr/src/db/docs/DocsV2Dao.ts | 871 +++-- servers/fdr/src/db/docs/IndexSegmentDao.ts | 38 +- .../fdr/src/db/generators/CliVersionsDao.ts | 405 +- servers/fdr/src/db/generators/GeneratorDao.ts | 339 +- .../src/db/generators/GeneratorVersionsDao.ts | 510 +-- servers/fdr/src/db/generators/daoUtils.ts | 138 +- .../db/generators/noncifySemanticVersion.ts | 31 +- servers/fdr/src/db/git/GitDao.ts | 658 ++-- .../db/registrations/DocsRegistrationDao.ts | 81 +- servers/fdr/src/db/sdk/SdkDao.ts | 368 +- .../fdr/src/db/snippetApis/SnippetAPIsDao.ts | 116 +- .../db/snippets/EndpointSnippetCollectors.ts | 76 +- servers/fdr/src/db/snippets/SdkIdFactory.ts | 30 +- .../fdr/src/db/snippets/SnippetTemplate.ts | 647 ++-- servers/fdr/src/db/snippets/SnippetsDao.ts | 754 ++-- .../getPackageNameFromSdkSnippetsCreate.ts | 147 +- servers/fdr/src/db/types.ts | 4 +- servers/fdr/src/healthchecks/checkRedis.ts | 137 +- servers/fdr/src/server.ts | 172 +- .../AlgoliaIndexSegmentDeleterService.ts | 131 +- .../algolia-index-segment-deleter/index.ts | 4 +- .../AlgoliaIndexSegmentManagerService.ts | 396 +- .../algolia-index-segment-manager/index.ts | 4 +- .../algolia/AlgoliaSearchRecordGenerator.ts | 1937 +++++----- .../algolia/AlgoliaSearchRecordGeneratorV2.ts | 3419 +++++++++-------- .../src/services/algolia/AlgoliaService.ts | 115 +- .../src/services/algolia/NavigationContext.ts | 116 +- .../services/algolia/getAllReferencedTypes.ts | 281 +- servers/fdr/src/services/algolia/index.ts | 6 +- servers/fdr/src/services/algolia/types.ts | 90 +- servers/fdr/src/services/auth/AuthService.ts | 316 +- .../fdr/src/services/db/DatabaseService.ts | 48 +- .../docs-cache/DocsDefinitionCache.ts | 475 +-- .../docs-cache/LocalDocsDefinitionStore.ts | 28 +- .../docs-cache/RedisDocsDefinitionStore.ts | 80 +- .../revalidator/RevalidatorService.ts | 165 +- .../fdr/src/services/revalidator/Semaphore.ts | 54 +- servers/fdr/src/services/s3/S3Service.ts | 544 +-- .../fdr/src/services/s3/__test__/s3.test.ts | 149 +- servers/fdr/src/services/s3/index.ts | 6 +- .../fdr/src/services/slack/SlackService.ts | 214 +- servers/fdr/src/types.ts | 4 +- servers/fdr/src/util/ParsedBaseUrl.ts | 67 +- servers/fdr/src/util/WithoutQuestionMarks.ts | 2 +- servers/fdr/src/util/assertNever.ts | 2 +- servers/fdr/src/util/bytes.ts | 64 +- servers/fdr/src/util/getFilesV2.ts | 79 +- servers/fdr/src/util/markdown.ts | 4 +- servers/fdr/src/util/object.ts | 48 +- servers/fdr/src/util/serde.ts | 16 +- servers/fdr/vitest.config.ts | 12 +- servers/fern-bot/.prettierrc.json | 12 - servers/fern-bot/package.json | 24 +- .../fern-bot/src/__test__/basic-ete.test.ts | 278 +- .../fern-bot/src/__test__/grpc-proxy.test.ts | 237 +- .../actions/updateGeneratorVersion.ts | 35 +- .../actions/updateGeneratorVersions.ts | 46 +- .../shared/updateGeneratorInternal.ts | 702 ++-- .../updateGeneratorVersion.ts | 31 +- .../updateGeneratorVersions.ts | 17 +- .../functions/grpc-proxy/actions/constants.ts | 2 +- .../functions/grpc-proxy/actions/proxyGrpc.ts | 144 +- .../src/functions/grpc-proxy/proxyGrpc.ts | 36 +- .../oas-cron/actions/updateOpenApiSpec.ts | 29 +- .../oas-cron/actions/updateOpenApiSpecs.ts | 56 +- .../oas-cron/shared/updateSpecInternal.ts | 104 +- .../functions/oas-cron/updateOpenApiSpec.ts | 13 +- .../functions/oas-cron/updateOpenApiSpecs.ts | 13 +- .../actions/sendStaleNotifications.ts | 177 +- .../stale-notifs/sendStaleNotifications.ts | 13 +- .../actions/updateFDRRepoData.ts | 387 +- .../update-fdr-repo-data/updateFDRRepoData.ts | 13 +- .../actions/updateRepoData.ts | 54 +- .../update-repo-data/updateRepoData.ts | 13 +- servers/fern-bot/src/libs/buf.ts | 197 +- servers/fern-bot/src/libs/cohere.ts | 70 +- servers/fern-bot/src/libs/env.ts | 90 +- servers/fern-bot/src/libs/fern.ts | 123 +- servers/fern-bot/src/libs/fs.ts | 24 +- servers/fern-bot/src/libs/github/octokit.ts | 40 +- .../fern-bot/src/libs/github/octokitHooks.ts | 100 +- servers/fern-bot/src/libs/github/utilities.ts | 52 +- servers/fern-bot/src/libs/handler-wrapper.ts | 96 +- servers/fern-bot/src/libs/schemas/RepoData.ts | 10 +- .../fern-bot/src/libs/slack/SlackService.ts | 685 ++-- .../src/utils/createLoggingExecutable.ts | 17 +- servers/fern-bot/src/utils/fetchAndUnzip.ts | 85 +- servers/fern-bot/src/utils/loggingExeca.ts | 59 +- servers/fern-bot/tsconfig.eslint.json | 8 +- servers/fern-bot/vitest.config.ts | 16 +- 172 files changed, 18096 insertions(+), 16089 deletions(-) delete mode 100644 packages/fern-docs/bundle/__mocks__/fileMock.js delete mode 100644 packages/fern-docs/bundle/__mocks__/styleMock.js delete mode 100644 packages/fern-docs/ui/src/__mocks__/fileMock.js delete mode 100644 packages/fern-docs/ui/src/__mocks__/styleMock.js delete mode 100644 servers/fdr/.prettierrc.json delete mode 100644 servers/fern-bot/.prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json index 8d3703cc74..57317be3cb 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,5 @@ { "trailingComma": "es5", - "plugins": ["prettier-plugin-packagejson", "prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-packagejson", "prettier-plugin-tailwindcss"], + "tailwindFunctions": ["clsx", "cn", "cva"] } diff --git a/eslint.config.mjs b/eslint.config.mjs index 67207c1ca8..19f9edcf9f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,6 +36,7 @@ export default tseslint.config( ...compat.config({ extends: [ + "prettier", "next/core-web-vitals", "next/typescript", "plugin:tailwindcss/recommended", @@ -111,6 +112,7 @@ export default tseslint.config( }, ], "tailwindcss/no-custom-classname": "off", + "tailwindcss/classnames-order": "off", }, }, diff --git a/packages/fdr-sdk/package.json b/packages/fdr-sdk/package.json index 9ef459cec6..d28feeb17d 100644 --- a/packages/fdr-sdk/package.json +++ b/packages/fdr-sdk/package.json @@ -68,7 +68,6 @@ "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/preset-typescript": "^7.26.0", - "@fern-api/eslint-config": "workspace:*", "@fern-platform/configs": "workspace:*", "@types/node-fetch": "2.6.9", "@types/qs": "6.9.14", diff --git a/packages/fern-docs/bundle/__mocks__/fileMock.js b/packages/fern-docs/bundle/__mocks__/fileMock.js deleted file mode 100644 index 0a445d0600..0000000000 --- a/packages/fern-docs/bundle/__mocks__/fileMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = "test-file-stub"; diff --git a/packages/fern-docs/bundle/__mocks__/styleMock.js b/packages/fern-docs/bundle/__mocks__/styleMock.js deleted file mode 100644 index f053ebf797..0000000000 --- a/packages/fern-docs/bundle/__mocks__/styleMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/packages/fern-docs/bundle/tsconfig.eslint.json b/packages/fern-docs/bundle/tsconfig.eslint.json index 7c7552d270..0570fca30a 100644 --- a/packages/fern-docs/bundle/tsconfig.eslint.json +++ b/packages/fern-docs/bundle/tsconfig.eslint.json @@ -5,7 +5,6 @@ "next.config.mjs", "postcss.config.js", "tailwind.config.js", - "vitest.config.ts", - "__mocks__/**/*" + "vitest.config.ts" ] } diff --git a/packages/fern-docs/bundle/tsconfig.json b/packages/fern-docs/bundle/tsconfig.json index c44b17ccd6..3d88b0649b 100644 --- a/packages/fern-docs/bundle/tsconfig.json +++ b/packages/fern-docs/bundle/tsconfig.json @@ -8,7 +8,7 @@ "strictNullChecks": true }, "exclude": ["node_modules"], - "include": ["./src/**/*", ".next/types/**/*.ts", "__mocks__"], + "include": ["./src/**/*", ".next/types/**/*.ts"], "references": [ { "path": "../../commons/core-utils" diff --git a/packages/fern-docs/components/src/FernButtonV2.tsx b/packages/fern-docs/components/src/FernButtonV2.tsx index 22931de352..e50aaf4bde 100644 --- a/packages/fern-docs/components/src/FernButtonV2.tsx +++ b/packages/fern-docs/components/src/FernButtonV2.tsx @@ -1,11 +1,12 @@ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; +import React from "react"; import { cn } from "./cn"; const buttonVariants = cva( cn( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors hover:transition-none", - "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + "focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-1", "disabled:pointer-events-none disabled:opacity-50", "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0" ), @@ -17,7 +18,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm", outline: - "border-input border bg-background shadow-sm hover:bg-[var(--grayscale-2)] hover:text-[var(--accent-12)]", + "border-input bg-background border shadow-sm hover:bg-[var(--grayscale-2)] hover:text-[var(--accent-12)]", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm", ghost: diff --git a/packages/fern-docs/icons-cdn/tsconfig.json b/packages/fern-docs/icons-cdn/tsconfig.json index 8ab9b01d2f..eee055e698 100644 --- a/packages/fern-docs/icons-cdn/tsconfig.json +++ b/packages/fern-docs/icons-cdn/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": "." }, "exclude": ["node_modules"], - "include": ["./src/**/*", ".next/types/**/*.ts", "__mocks__"] + "include": ["./src/**/*", ".next/types/**/*.ts"] } diff --git a/packages/fern-docs/search-ui/src/components/desktop/desktop-search-button.tsx b/packages/fern-docs/search-ui/src/components/desktop/desktop-search-button.tsx index 31b52e2071..1a563eb287 100644 --- a/packages/fern-docs/search-ui/src/components/desktop/desktop-search-button.tsx +++ b/packages/fern-docs/search-ui/src/components/desktop/desktop-search-button.tsx @@ -5,14 +5,14 @@ import { ComponentPropsWithoutRef, forwardRef, memo } from "react"; import { cn } from "../ui/cn"; const buttonVariants = cva( - "inline-flex items-center justify-start gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors hover:transition-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--accent-6)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 h-9 p-2 w-full cursor-text", + "inline-flex h-9 w-full cursor-text items-center justify-start gap-2 whitespace-nowrap rounded-md p-2 text-sm font-medium transition-colors hover:transition-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--accent-6)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: "bg-[var(--grayscale-a3)] text-[var(--grayscale-a10)] hover:bg-[var(--grayscale-a4)]", loading: - "bg-[var(--grayscale-a3)] text-[var(--grayscale-a10)] cursor-default", + "cursor-default bg-[var(--grayscale-a3)] text-[var(--grayscale-a10)]", }, }, defaultVariants: { diff --git a/packages/fern-docs/search-ui/src/components/ui/sheet.tsx b/packages/fern-docs/search-ui/src/components/ui/sheet.tsx index a712089376..892df3c9bd 100644 --- a/packages/fern-docs/search-ui/src/components/ui/sheet.tsx +++ b/packages/fern-docs/search-ui/src/components/ui/sheet.tsx @@ -29,16 +29,16 @@ const SheetOverlay = React.forwardRef< SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - "fixed z-50 gap-4 bg-[var(--grayscale-1)] p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", + "data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 bg-[var(--grayscale-1)] p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", { variants: { side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b", bottom: - "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t", + left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", right: - "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", }, }, defaultVariants: { diff --git a/packages/fern-docs/ui/src/__mocks__/fileMock.js b/packages/fern-docs/ui/src/__mocks__/fileMock.js deleted file mode 100644 index 0a445d0600..0000000000 --- a/packages/fern-docs/ui/src/__mocks__/fileMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = "test-file-stub"; diff --git a/packages/fern-docs/ui/src/__mocks__/styleMock.js b/packages/fern-docs/ui/src/__mocks__/styleMock.js deleted file mode 100644 index f053ebf797..0000000000 --- a/packages/fern-docs/ui/src/__mocks__/styleMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/packages/fern-docs/ui/src/api-reference/endpoints/EndpointUrl.tsx b/packages/fern-docs/ui/src/api-reference/endpoints/EndpointUrl.tsx index 1d2cdfeb95..122f547953 100644 --- a/packages/fern-docs/ui/src/api-reference/endpoints/EndpointUrl.tsx +++ b/packages/fern-docs/ui/src/api-reference/endpoints/EndpointUrl.tsx @@ -48,7 +48,7 @@ export const EndpointUrl = React.forwardRef< ) { const ref = useRef(null); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion useImperativeHandle(parentRef, () => ref.current!); const [isHovered, setIsHovered] = useState(false); diff --git a/packages/fern-docs/ui/src/mdx/components/card/Card.tsx b/packages/fern-docs/ui/src/mdx/components/card/Card.tsx index 95416bd1eb..4e0623a8cd 100644 --- a/packages/fern-docs/ui/src/mdx/components/card/Card.tsx +++ b/packages/fern-docs/ui/src/mdx/components/card/Card.tsx @@ -35,7 +35,7 @@ export const Card: React.FC = ({ badge, }) => { const className = cn( - "text-base border p-6 not-prose rounded-xl relative block" + "not-prose relative block rounded-xl border p-6 text-base" ); const content = ( diff --git a/packages/fern-docs/ui/src/mdx/components/code/CodeGroup.tsx b/packages/fern-docs/ui/src/mdx/components/code/CodeGroup.tsx index 3d73561e74..5b56fd77ff 100644 --- a/packages/fern-docs/ui/src/mdx/components/code/CodeGroup.tsx +++ b/packages/fern-docs/ui/src/mdx/components/code/CodeGroup.tsx @@ -28,7 +28,7 @@ export const CodeGroup: React.FC> = ({ const containerClass = clsx( "bg-card after:ring-card-border relative mb-6 mt-4 flex w-full min-w-0 max-w-full flex-col rounded-lg shadow-sm after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:ring-1 after:ring-inset after:content-[''] first:mt-0", { - "dark bg-card-solid": isDarkCodeEnabled, + "bg-card-solid dark": isDarkCodeEnabled, } ); diff --git a/packages/template-resolver/src/formatSnippet.ts b/packages/template-resolver/src/formatSnippet.ts index a97b2a22ec..d43547a903 100644 --- a/packages/template-resolver/src/formatSnippet.ts +++ b/packages/template-resolver/src/formatSnippet.ts @@ -1,6 +1,6 @@ import { FernRegistry } from "@fern-fern/fdr-cjs-sdk"; import parserBabel from "prettier/plugins/babel"; -import * as prettierPluginEstree from "prettier/plugins/estree"; +import prettierPluginEstree from "prettier/plugins/estree"; import * as prettier from "prettier/standalone"; export async function formatSnippet( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ad986774f..c4f4457a23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -587,55 +587,6 @@ importers: packages/configs: {} - packages/eslint: - dependencies: - '@eslint/compat': - specifier: ^1.2.4 - version: 1.2.4(eslint@9.17.0(jiti@1.21.7)) - '@eslint/eslintrc': - specifier: ^3.2.0 - version: 3.2.0 - '@eslint/js': - specifier: ^9.17.0 - version: 9.17.0 - '@typescript-eslint/eslint-plugin': - specifier: 8.18.1 - version: 8.18.1(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2) - '@typescript-eslint/parser': - specifier: 8.18.1 - version: 8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2) - eslint: - specifier: 9.17.0 - version: 9.17.0(jiti@1.21.7) - eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@9.17.0(jiti@1.21.7)) - eslint-plugin-deprecation: - specifier: ^3.0.0 - version: 3.0.0(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2) - eslint-plugin-import: - specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@9.17.0(jiti@1.21.7)) - eslint-plugin-react: - specifier: ^7.37.2 - version: 7.37.2(eslint@9.17.0(jiti@1.21.7)) - eslint-plugin-react-hooks: - specifier: ^5.1.0 - version: 5.1.0(eslint@9.17.0(jiti@1.21.7)) - eslint-plugin-tailwindcss: - specifier: ^3.17.5 - version: 3.17.5(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.5.7)(@types/node@18.19.33)(typescript@5.7.2))) - eslint-plugin-vitest: - specifier: ^0.5.4 - version: 0.5.4(@typescript-eslint/eslint-plugin@8.18.1(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2)(vitest@2.1.4(@edge-runtime/vm@3.2.0)(@types/node@18.19.33)(jsdom@24.0.0)(less@4.2.0)(sass@1.77.0)(stylus@0.62.0)(terser@5.31.0)) - globals: - specifier: ^15.14.0 - version: 15.14.0 - devDependencies: - prettier: - specifier: ^3.4.2 - version: 3.4.2 - packages/fdr-sdk: dependencies: '@fern-api/ui-core-utils': @@ -690,9 +641,6 @@ importers: '@babel/preset-typescript': specifier: ^7.26.0 version: 7.26.0(@babel/core@7.26.0) - '@fern-api/eslint-config': - specifier: workspace:* - version: link:../eslint '@fern-platform/configs': specifier: workspace:* version: link:../configs diff --git a/servers/fdr/.prettierrc.json b/servers/fdr/.prettierrc.json deleted file mode 100644 index 869fea81da..0000000000 --- a/servers/fdr/.prettierrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 4, - "overrides": [ - { - "files": "*.{yml,yaml,json,md,mdx}", - "options": { - "tabWidth": 2 - }, - "excludeFiles": [ - "src/__test__/unit-tests/generate-algolia-search-records/fixtures/vapi/apis/031f13c0-3070-48ca-bfcf-90aa72f935d8.json" - ] - } - ] -} diff --git a/servers/fdr/package.json b/servers/fdr/package.json index e9df288831..8c63a63101 100644 --- a/servers/fdr/package.json +++ b/servers/fdr/package.json @@ -1,14 +1,38 @@ { "name": "@fern-platform/fdr", "version": "0.0.0", - "main": "dist/index.js", "repository": "git@github.com:fern-api/fern-definition-registry.git", - "author": "Deep Singhvi ", "license": "MIT", + "author": "Deep Singhvi ", + "type": "module", + "main": "dist/index.js", "files": [ "dist" ], - "type": "module", + "scripts": { + "clean": "rm -rf ./dist && tsc --build --clean", + "codegen": "prisma generate", + "compile": "tsc --build", + "db:down:local": "docker-compose -f docker-compose.local.yml down", + "db:migrate:dev": "dotenv -e .env.dev -- prisma migrate deploy", + "db:migrate:local": "dotenv -e .env.migrate -- prisma migrate dev", + "db:migrate:prod": "dotenv -e .env.prod -- prisma migrate deploy", + "db:up:local": "docker-compose -f docker-compose.local.yml up -d", + "dev": "tsx src/server.ts --watch", + "devjs": "node --experimental-specifier-resolution=node dist/server.js", + "docker:dev": "dotenv -e .env.dev -- ./create_docker.sh", + "docker:local": "dotenv -e .env.test -- ./create_docker.sh local", + "docker:prod": "dotenv -e .env.prod -- ./create_docker.sh", + "format": "prettier --write --ignore-unknown \"**\"", + "format:check": "prettier --check --ignore-unknown \"**\"", + "format:prisma": "prisma format", + "lint:eslint": "eslint --max-warnings 0 .", + "lint:eslint:fix": "pnpm lint:eslint --fix", + "test": "vitest --run src/** --globals", + "test:ete": "dotenv -e .env.ete -- vitest src/__test__/ete --globals --config src/__test__/ete/vitest.config.ts", + "test:local": "dotenv -e .env.test -- vitest src/__test__/local --globals --config src/__test__/local/vitest.config.ts --no-file-parallelism", + "test:update": "vitest --run src/** --globals -u" + }, "dependencies": { "@aws-sdk/client-s3": "^3.685.0", "@aws-sdk/s3-request-presigner": "^3.685.0", @@ -17,9 +41,9 @@ "@fern-api/template-resolver": "workspace:*", "@fern-api/ui-core-utils": "workspace:*", "@fern-api/venus-api-sdk": "^0.10.1-5-ged06d22", + "@fern-docs/search-server": "workspace:*", "@fern-fern/fern-docs-sdk": "0.0.5", "@fern-fern/revalidation-sdk": "0.0.9", - "@fern-docs/search-server": "workspace:*", "@prisma/client": "5.13.0", "@sentry/cli": "^2.31.0", "@sentry/node": "^7.112.2", @@ -73,29 +97,5 @@ "tsx": "^4.7.1", "typescript": "^5", "vitest": "^2.1.4" - }, - "scripts": { - "compile": "tsc --build", - "codegen": "prisma generate", - "db:migrate:dev": "dotenv -e .env.dev -- prisma migrate deploy", - "db:migrate:local": "dotenv -e .env.migrate -- prisma migrate dev", - "db:migrate:prod": "dotenv -e .env.prod -- prisma migrate deploy", - "db:up:local": "docker-compose -f docker-compose.local.yml up -d", - "db:down:local": "docker-compose -f docker-compose.local.yml down", - "dev": "tsx src/server.ts --watch", - "devjs": "node --experimental-specifier-resolution=node dist/server.js", - "clean": "rm -rf ./dist && tsc --build --clean", - "format": "prettier --write --ignore-unknown \"**\"", - "format:check": "prettier --check --ignore-unknown \"**\"", - "format:prisma": "prisma format", - "docker:local": "dotenv -e .env.test -- ./create_docker.sh local", - "docker:dev": "dotenv -e .env.dev -- ./create_docker.sh", - "docker:prod": "dotenv -e .env.prod -- ./create_docker.sh", - "test": "vitest --run src/** --globals", - "test:update": "vitest --run src/** --globals -u", - "lint:eslint": "eslint --max-warnings 0 .", - "lint:eslint:fix": "pnpm lint:eslint --fix", - "test:local": "dotenv -e .env.test -- vitest src/__test__/local --globals --config src/__test__/local/vitest.config.ts --no-file-parallelism", - "test:ete": "dotenv -e .env.ete -- vitest src/__test__/ete --globals --config src/__test__/ete/vitest.config.ts" } } diff --git a/servers/fdr/src/Cache.ts b/servers/fdr/src/Cache.ts index 35567fe747..823f73177e 100644 --- a/servers/fdr/src/Cache.ts +++ b/servers/fdr/src/Cache.ts @@ -1,91 +1,91 @@ const MILLIS_IN_SECOND = 1000; export class Cache { - private readonly cache: Record = {}; + private readonly cache: Record = {}; - // keysQueue is used to keep track of the order in which keys were added to the cache - private keysQueue: string[] = []; + // keysQueue is used to keep track of the order in which keys were added to the cache + private keysQueue: string[] = []; - public constructor( - private readonly maxKeys: number, - private readonly ttl?: number /* in seconds */, - ) {} + public constructor( + private readonly maxKeys: number, + private readonly ttl?: number /* in seconds */ + ) {} - public get(key: string): V | undefined { - const item = this.cache[key]; - if (item != null) { - // if ttl is not set, or if it is set to a negative value, then return the value - if (this.ttl == null || this.ttl < 0) { - return item.value; - } + public get(key: string): V | undefined { + const item = this.cache[key]; + if (item != null) { + // if ttl is not set, or if it is set to a negative value, then return the value + if (this.ttl == null || this.ttl < 0) { + return item.value; + } - const now = Date.now(); - if (now - item.timestamp < this.ttl * MILLIS_IN_SECOND) { - return item.value; - } else { - // remove the key from the queue and all keys before it - const index = this.keysQueue.indexOf(key); - this.deleteIndex(index); - } - } - return undefined; + const now = Date.now(); + if (now - item.timestamp < this.ttl * MILLIS_IN_SECOND) { + return item.value; + } else { + // remove the key from the queue and all keys before it + const index = this.keysQueue.indexOf(key); + this.deleteIndex(index); + } } + return undefined; + } - public set(key: string, value: V): void { - this.deleteOldestKeys(); + public set(key: string, value: V): void { + this.deleteOldestKeys(); - this.cache[key] = { - value, - timestamp: Date.now(), - }; + this.cache[key] = { + value, + timestamp: Date.now(), + }; - // move the key to the end of the queue - this.keysQueue = this.keysQueue.filter((k) => k !== key); - this.keysQueue.push(key); - } + // move the key to the end of the queue + this.keysQueue = this.keysQueue.filter((k) => k !== key); + this.keysQueue.push(key); + } - private deleteOldestKeys(): void { - // remove the oldest keys if the cache is full, and makes space for a new key - while (this.keysQueue.length > this.maxKeys - 1) { - const key = this.keysQueue.shift(); - if (key == null) { - // this should never happen - continue; - } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.cache[key]; - } + private deleteOldestKeys(): void { + // remove the oldest keys if the cache is full, and makes space for a new key + while (this.keysQueue.length > this.maxKeys - 1) { + const key = this.keysQueue.shift(); + if (key == null) { + // this should never happen + continue; + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.cache[key]; + } - const ttl = this.ttl; - if (ttl == null) { - return; - } + const ttl = this.ttl; + if (ttl == null) { + return; + } - // remove all keys that have expired - const index = this.keysQueue.findIndex((key) => { - const item = this.cache[key]; - if (item == null) { - return false; - } - return Date.now() - item.timestamp >= ttl * MILLIS_IN_SECOND; - }); + // remove all keys that have expired + const index = this.keysQueue.findIndex((key) => { + const item = this.cache[key]; + if (item == null) { + return false; + } + return Date.now() - item.timestamp >= ttl * MILLIS_IN_SECOND; + }); - if (index < 1) { - return; - } - this.deleteIndex(index - 1); + if (index < 1) { + return; } + this.deleteIndex(index - 1); + } - private deleteIndex(index: number): void { - if (index < 0) { - return; - } - const keysToDelete = this.keysQueue.slice(0, index); - this.keysQueue = this.keysQueue.slice(index); + private deleteIndex(index: number): void { + if (index < 0) { + return; + } + const keysToDelete = this.keysQueue.slice(0, index); + this.keysQueue = this.keysQueue.slice(index); - for (const key of keysToDelete) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.cache[key]; - } + for (const key of keysToDelete) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.cache[key]; } + } } diff --git a/servers/fdr/src/__test__/ete/ete.test.ts b/servers/fdr/src/__test__/ete/ete.test.ts index e867bac5cb..ea7bb9139f 100644 --- a/servers/fdr/src/__test__/ete/ete.test.ts +++ b/servers/fdr/src/__test__/ete/ete.test.ts @@ -3,24 +3,24 @@ const PORT = 8080; it("check health", async () => { - // register empty definition + // register empty definition - for (let i = 0; i < 10; ++i) { - await sleep(20_000); - try { - const response = await fetch(`http://localhost:${PORT}/health`); - if (response.status === 200) { - return; - } else { - console.log(`Received status ${response.status}`); - } - } catch (err) { - console.log(err); - } + for (let i = 0; i < 10; ++i) { + await sleep(20_000); + try { + const response = await fetch(`http://localhost:${PORT}/health`); + if (response.status === 200) { + return; + } else { + console.log(`Received status ${response.status}`); + } + } catch (err) { + console.log(err); } - throw new Error("Failed to make successfull request"); + } + throw new Error("Failed to make successfull request"); }, 100_000); function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/servers/fdr/src/__test__/ete/setupDockerizedFdr.ts b/servers/fdr/src/__test__/ete/setupDockerizedFdr.ts index 511580a6fe..fbcaca7eba 100644 --- a/servers/fdr/src/__test__/ete/setupDockerizedFdr.ts +++ b/servers/fdr/src/__test__/ete/setupDockerizedFdr.ts @@ -3,21 +3,25 @@ import { execa } from "execa"; let teardown = false; export async function setup() { - await execa("pnpm", ["docker:local"], { stdio: "inherit" }); - await execa("docker-compose", ["-f", "docker-compose.ete.yml", "up", "-d"], { stdio: "inherit" }); - await sleep(10000); - return async () => { - if (teardown) { - throw new Error("teardown called twice"); - } - teardown = true; - await execa("docker-compose", ["-f", "docker-compose.ete.yml", "down"], { stdio: "inherit" }); - return new Promise((resolve) => { - resolve(); - }); - }; + await execa("pnpm", ["docker:local"], { stdio: "inherit" }); + await execa("docker-compose", ["-f", "docker-compose.ete.yml", "up", "-d"], { + stdio: "inherit", + }); + await sleep(10000); + return async () => { + if (teardown) { + throw new Error("teardown called twice"); + } + teardown = true; + await execa("docker-compose", ["-f", "docker-compose.ete.yml", "down"], { + stdio: "inherit", + }); + return new Promise((resolve) => { + resolve(); + }); + }; } function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/servers/fdr/src/__test__/ete/vitest.config.ts b/servers/fdr/src/__test__/ete/vitest.config.ts index a00b63b6ba..9547dcbf85 100644 --- a/servers/fdr/src/__test__/ete/vitest.config.ts +++ b/servers/fdr/src/__test__/ete/vitest.config.ts @@ -1,8 +1,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - globalSetup: ["src/__test__/ete/setupDockerizedFdr.ts"], - }, + test: { + globals: true, + globalSetup: ["src/__test__/ete/setupDockerizedFdr.ts"], + }, }); diff --git a/servers/fdr/src/__test__/local/algolia.test.ts b/servers/fdr/src/__test__/local/algolia.test.ts index 43c157eea4..e054950a11 100644 --- a/servers/fdr/src/__test__/local/algolia.test.ts +++ b/servers/fdr/src/__test__/local/algolia.test.ts @@ -2,63 +2,100 @@ import { addHours, subHours } from "date-fns"; import { uniqueId } from "es-toolkit/compat"; import { createMockFdrApplication } from "../mock"; import { prisma } from "./setupMockFdr"; -import { createMockDocs, createMockIndexSegment, getUniqueDocsForUrl } from "./util"; +import { + createMockDocs, + createMockIndexSegment, + getUniqueDocsForUrl, +} from "./util"; function getUniqueSegment(): string { - return `seg_${Math.random()}`; + return `seg_${Math.random()}`; } const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], + orgIds: ["acme", "octoai"], }); describe("algolia index segment deleter", () => { - it("correctly deletes old inactive index segments for unversioned docs", async () => { - const domain = getUniqueDocsForUrl("algolia"); - const path = "abc"; + it("correctly deletes old inactive index segments for unversioned docs", async () => { + const domain = getUniqueDocsForUrl("algolia"); + const path = "abc"; - // Index segments that were deleted before this date are considered "dated" or "old" - // Fern only deletes old segments that are not referenced by any docs - const olderThanHours = 24; - const oldSegmentCutoffDate = subHours(new Date(), olderThanHours); + // Index segments that were deleted before this date are considered "dated" or "old" + // Fern only deletes old segments that are not referenced by any docs + const olderThanHours = 24; + const oldSegmentCutoffDate = subHours(new Date(), olderThanHours); - console.log(uniqueId("seg")); + console.log(uniqueId("seg")); - const inactiveOldIndexSegments = [ - createMockIndexSegment({ id: getUniqueSegment(), createdAt: subHours(oldSegmentCutoffDate, 4) }), - createMockIndexSegment({ id: getUniqueSegment(), createdAt: subHours(oldSegmentCutoffDate, 3) }), - createMockIndexSegment({ id: getUniqueSegment(), createdAt: subHours(oldSegmentCutoffDate, 2) }), - createMockIndexSegment({ id: getUniqueSegment(), createdAt: subHours(oldSegmentCutoffDate, 1) }), - ]; - const activeIndexSegments = [ - createMockIndexSegment({ id: getUniqueSegment(), createdAt: addHours(oldSegmentCutoffDate, 1) }), - ]; - const docsV2 = createMockDocs({ domain, path, indexSegmentIds: activeIndexSegments.map((s) => s.id) }); + const inactiveOldIndexSegments = [ + createMockIndexSegment({ + id: getUniqueSegment(), + createdAt: subHours(oldSegmentCutoffDate, 4), + }), + createMockIndexSegment({ + id: getUniqueSegment(), + createdAt: subHours(oldSegmentCutoffDate, 3), + }), + createMockIndexSegment({ + id: getUniqueSegment(), + createdAt: subHours(oldSegmentCutoffDate, 2), + }), + createMockIndexSegment({ + id: getUniqueSegment(), + createdAt: subHours(oldSegmentCutoffDate, 1), + }), + ]; + const activeIndexSegments = [ + createMockIndexSegment({ + id: getUniqueSegment(), + createdAt: addHours(oldSegmentCutoffDate, 1), + }), + ]; + const docsV2 = createMockDocs({ + domain, + path, + indexSegmentIds: activeIndexSegments.map((s) => s.id), + }); - await prisma.$transaction(async (tx) => { - await tx.docsV2.create({ data: { ...docsV2, indexSegmentIds: docsV2.indexSegmentIds as string[] } }); - const allIndexSegments = [...inactiveOldIndexSegments, ...activeIndexSegments]; - await Promise.all(allIndexSegments.map((seg) => tx.indexSegment.create({ data: seg }))); - }); + await prisma.$transaction(async (tx) => { + await tx.docsV2.create({ + data: { + ...docsV2, + indexSegmentIds: docsV2.indexSegmentIds as string[], + }, + }); + const allIndexSegments = [ + ...inactiveOldIndexSegments, + ...activeIndexSegments, + ]; + await Promise.all( + allIndexSegments.map((seg) => tx.indexSegment.create({ data: seg })) + ); + }); - await fdrApplication.services.algoliaIndexSegmentDeleter.deleteOldInactiveIndexSegments({ - olderThanHours, - }); + await fdrApplication.services.algoliaIndexSegmentDeleter.deleteOldInactiveIndexSegments( + { + olderThanHours, + } + ); - const newIndexSegmentRecords = await prisma.indexSegment.findMany({ - select: { id: true }, - }); + const newIndexSegmentRecords = await prisma.indexSegment.findMany({ + select: { id: true }, + }); - const newIndexSegmentRecordIds = new Set(newIndexSegmentRecords.map((r) => r.id)); + const newIndexSegmentRecordIds = new Set( + newIndexSegmentRecords.map((r) => r.id) + ); - // Expect inactive old segments to be deleted - inactiveOldIndexSegments.forEach((s) => { - expect(newIndexSegmentRecordIds.has(s.id)).toBe(false); - }); + // Expect inactive old segments to be deleted + inactiveOldIndexSegments.forEach((s) => { + expect(newIndexSegmentRecordIds.has(s.id)).toBe(false); + }); - // Expect active segments to remain - activeIndexSegments.forEach((s) => { - expect(newIndexSegmentRecordIds.has(s.id)).toBe(true); - }); + // Expect active segments to remain + activeIndexSegments.forEach((s) => { + expect(newIndexSegmentRecordIds.has(s.id)).toBe(true); }); + }); }); diff --git a/servers/fdr/src/__test__/local/db/cliVersionsDao.test.ts b/servers/fdr/src/__test__/local/db/cliVersionsDao.test.ts index 05a2977697..333424c193 100644 --- a/servers/fdr/src/__test__/local/db/cliVersionsDao.test.ts +++ b/servers/fdr/src/__test__/local/db/cliVersionsDao.test.ts @@ -1,419 +1,441 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; -import { CliReleaseRequest, InvalidVersionError, ReleaseType } from "../../../api/generated/api/resources/generators"; +import { + CliReleaseRequest, + InvalidVersionError, + ReleaseType, +} from "../../../api/generated/api/resources/generators"; import { noncifySemanticVersion } from "../../../db/generators/noncifySemanticVersion"; import { createMockFdrApplication } from "../../mock"; const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], + orgIds: ["acme", "octoai"], }); function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } it("cli verion not semver", async () => { - await expect(async () => { - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - version: "abc.1.2", - irVersion: 0, - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); - }).rejects.toThrow(new InvalidVersionError({ providedVersion: "abc.1.2" })); -}); - -it("cli release with tags and URLs", async () => { - const release: CliReleaseRequest = { - version: "0.1.2-rc13", + await expect(async () => { + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + version: "abc.1.2", irVersion: 0, - tags: ["OpenAPI", "Fern Definition"], - changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - links: ["https://123.com"], - upgradeNotes: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - fixed: undefined, - }, - ], createdAt: undefined, isYanked: undefined, - }; - - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: release, + changelogEntry: undefined, + tags: undefined, + }, }); + }).rejects.toThrow(new InvalidVersionError({ providedVersion: "abc.1.2" })); +}); - const dbRelease = await fdrApplication.dao.cliVersions().getCliRelease({ cliVersion: "0.1.2-rc13" }); - expect(dbRelease).not.toBeUndefined(); - expect(dbRelease?.tags).toEqual(release.tags); - expect(dbRelease?.changelogEntry?.[0]?.links).toEqual(release.changelogEntry?.[0]?.links); +it("cli release with tags and URLs", async () => { + const release: CliReleaseRequest = { + version: "0.1.2-rc13", + irVersion: 0, + tags: ["OpenAPI", "Fern Definition"], + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + links: ["https://123.com"], + upgradeNotes: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, + fixed: undefined, + }, + ], + createdAt: undefined, + isYanked: undefined, + }; + + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: release, + }); + + const dbRelease = await fdrApplication.dao + .cliVersions() + .getCliRelease({ cliVersion: "0.1.2-rc13" }); + expect(dbRelease).not.toBeUndefined(); + expect(dbRelease?.tags).toEqual(release.tags); + expect(dbRelease?.changelogEntry?.[0]?.links).toEqual( + release.changelogEntry?.[0]?.links + ); }); it("cli version get latest respects semver, not time", async () => { - // create some versions and sleep between them to ensure the timestamps are different - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - version: "0.1.2", - irVersion: 0, - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); + // create some versions and sleep between them to ensure the timestamps are different + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + version: "0.1.2", + irVersion: 0, + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); - await delay(1000); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - version: "1.1.0", - irVersion: 1, - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); + await delay(1000); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + version: "1.1.0", + irVersion: 1, + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); - await delay(1000); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - version: "0.1.5", - irVersion: 0, - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); + await delay(1000); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + version: "0.1.5", + irVersion: 0, + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); - // update an old one too to impact update time - await delay(1000); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - version: "1.1.0-rc.1", - irVersion: 0, - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); + // update an old one too to impact update time + await delay(1000); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + version: "1.1.0-rc.1", + irVersion: 0, + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); - expect( - (await fdrApplication.dao.cliVersions().getLatestCliRelease({ getLatestCliReleaseRequest: {} }))?.version, - ).toEqual("1.1.0"); - expect( - ( - await fdrApplication.dao - .cliVersions() - .getLatestCliRelease({ getLatestCliReleaseRequest: { releaseTypes: [ReleaseType.Rc] } }) - )?.version, - ).toEqual("1.1.0-rc.1"); - expect( - ( - await fdrApplication.dao - .cliVersions() - .getLatestCliRelease({ getLatestCliReleaseRequest: { releaseTypes: [ReleaseType.Ga] } }) - )?.version, - ).toEqual("1.1.0"); + expect( + ( + await fdrApplication.dao + .cliVersions() + .getLatestCliRelease({ getLatestCliReleaseRequest: {} }) + )?.version + ).toEqual("1.1.0"); + expect( + ( + await fdrApplication.dao + .cliVersions() + .getLatestCliRelease({ + getLatestCliReleaseRequest: { releaseTypes: [ReleaseType.Rc] }, + }) + )?.version + ).toEqual("1.1.0-rc.1"); + expect( + ( + await fdrApplication.dao + .cliVersions() + .getLatestCliRelease({ + getLatestCliReleaseRequest: { releaseTypes: [ReleaseType.Ga] }, + }) + )?.version + ).toEqual("1.1.0"); }); it("generator changelog", async () => { - // create some versions and sleep between them to ensure the timestamps are different - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 1, - version: "2.1.2", - changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - links: undefined, - upgradeNotes: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - fixed: undefined, - }, - ], - createdAt: undefined, - isYanked: undefined, - tags: undefined, + // create some versions and sleep between them to ensure the timestamps are different + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 1, + version: "2.1.2", + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + links: undefined, + upgradeNotes: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, + fixed: undefined, }, - }); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 1, - version: "2.1.3", - changelogEntry: [ - { - type: "fix", - summary: "fixed that new feature", - fixed: ["fixed that new feature"], - links: undefined, - upgradeNotes: undefined, - added: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - }, - ], - createdAt: undefined, - isYanked: undefined, - tags: undefined, + ], + createdAt: undefined, + isYanked: undefined, + tags: undefined, + }, + }); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 1, + version: "2.1.3", + changelogEntry: [ + { + type: "fix", + summary: "fixed that new feature", + fixed: ["fixed that new feature"], + links: undefined, + upgradeNotes: undefined, + added: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, }, - }); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 1, - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a bunch of stuff", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - links: undefined, - upgradeNotes: undefined, - added: undefined, - changed: undefined, - removed: undefined, - }, - ], - createdAt: undefined, - isYanked: undefined, - tags: undefined, + ], + createdAt: undefined, + isYanked: undefined, + tags: undefined, + }, + }); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 1, + version: "2.1.5", + changelogEntry: [ + { + type: "fix", + summary: "did a bunch of stuff", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + links: undefined, + upgradeNotes: undefined, + added: undefined, + changed: undefined, + removed: undefined, }, - }); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 1, - version: "2.1.6", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); + ], + createdAt: undefined, + isYanked: undefined, + tags: undefined, + }, + }); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 1, + version: "2.1.6", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 0, - version: "2.1.8", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 0, + version: "2.1.8", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); - // Note we explicitly do not include 0.1.2 and 0.1.8 in the range to ensure we're only including the range - expect( - await fdrApplication.dao.cliVersions().getChangelog({ - versionRanges: { - fromVersion: { type: "inclusive", value: "2.1.3" }, - toVersion: { type: "inclusive", value: "2.1.7" }, - }, - }), - ).toEqual({ - entries: [ - { - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a bunch of stuff", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - }, - ], - }, - { - version: "2.1.3", - changelogEntry: [ - { - type: "fix", - summary: "fixed that new feature", - fixed: ["fixed that new feature"], - }, - ], - }, + // Note we explicitly do not include 0.1.2 and 0.1.8 in the range to ensure we're only including the range + expect( + await fdrApplication.dao.cliVersions().getChangelog({ + versionRanges: { + fromVersion: { type: "inclusive", value: "2.1.3" }, + toVersion: { type: "inclusive", value: "2.1.7" }, + }, + }) + ).toEqual({ + entries: [ + { + version: "2.1.5", + changelogEntry: [ + { + type: "fix", + summary: "did a bunch of stuff", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + }, ], - }); - // Should not get the minimum, given it's exclusive - expect( - await fdrApplication.dao.generatorVersions().getChangelog({ - generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), - versionRanges: { - fromVersion: { type: "exclusive", value: "2.1.3" }, - toVersion: { type: "exclusive", value: "2.1.7" }, - }, - }), - ).toEqual({ - entries: [ - { - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - }, - ], - }, + }, + { + version: "2.1.3", + changelogEntry: [ + { + type: "fix", + summary: "fixed that new feature", + fixed: ["fixed that new feature"], + }, ], - }); - - // Should get every changelog - expect( - await fdrApplication.dao.generatorVersions().getChangelog({ - generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), - versionRanges: { - fromVersion: { type: "inclusive", value: "2.1.2" }, - toVersion: { type: "inclusive", value: "2.1.8" }, - }, - }), - ).toEqual({ - entries: [ - { - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - }, - ], - }, - { - version: "2.1.3", - changelogEntry: [ - { - type: "fix", - summary: "fixed that new feature", - fixed: ["fixed that new feature"], - }, - ], - }, - { - version: "2.1.2", - changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - }, - ], - }, + }, + ], + }); + // Should not get the minimum, given it's exclusive + expect( + await fdrApplication.dao.generatorVersions().getChangelog({ + generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), + versionRanges: { + fromVersion: { type: "exclusive", value: "2.1.3" }, + toVersion: { type: "exclusive", value: "2.1.7" }, + }, + }) + ).toEqual({ + entries: [ + { + version: "2.1.5", + changelogEntry: [ + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + }, ], - }); -}); + }, + ], + }); -it("cli version happy path update", async () => { - const releaseRequest: CliReleaseRequest = { - irVersion: 0, - version: "3.1.2", + // Should get every changelog + expect( + await fdrApplication.dao.generatorVersions().getChangelog({ + generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), + versionRanges: { + fromVersion: { type: "inclusive", value: "2.1.2" }, + toVersion: { type: "inclusive", value: "2.1.8" }, + }, + }) + ).toEqual({ + entries: [ + { + version: "2.1.5", changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - links: undefined, - upgradeNotes: undefined, - added: undefined, - changed: undefined, - removed: undefined, - }, + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + }, ], - createdAt: undefined, - isYanked: undefined, - tags: undefined, - }; - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: releaseRequest, - }); - const release = await fdrApplication.dao.cliVersions().getCliRelease({ - cliVersion: "3.1.2", - }); - expect(release?.irVersion).toEqual(releaseRequest.irVersion); - expect(release?.version).toEqual(releaseRequest.version); - expect(release?.changelogEntry).toEqual(releaseRequest.changelogEntry); - - // Overwrite the release's changelog - const updateReleaseRequest: CliReleaseRequest = { - irVersion: 0, - version: "3.1.2", + }, + { + version: "2.1.3", changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - links: undefined, - upgradeNotes: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - fixed: undefined, - }, + { + type: "fix", + summary: "fixed that new feature", + fixed: ["fixed that new feature"], + }, ], - createdAt: undefined, - isYanked: undefined, - tags: undefined, - }; - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: updateReleaseRequest, - }); - const updatedRelease = await fdrApplication.dao.cliVersions().getCliRelease({ - cliVersion: "3.1.2", - }); - expect(updatedRelease?.irVersion).toEqual(updateReleaseRequest.irVersion); - expect(updatedRelease?.version).toEqual(updateReleaseRequest.version); - expect(updatedRelease?.changelogEntry).toEqual(updateReleaseRequest.changelogEntry); + }, + { + version: "2.1.2", + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + }, + ], + }, + ], + }); +}); + +it("cli version happy path update", async () => { + const releaseRequest: CliReleaseRequest = { + irVersion: 0, + version: "3.1.2", + changelogEntry: [ + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + links: undefined, + upgradeNotes: undefined, + added: undefined, + changed: undefined, + removed: undefined, + }, + ], + createdAt: undefined, + isYanked: undefined, + tags: undefined, + }; + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: releaseRequest, + }); + const release = await fdrApplication.dao.cliVersions().getCliRelease({ + cliVersion: "3.1.2", + }); + expect(release?.irVersion).toEqual(releaseRequest.irVersion); + expect(release?.version).toEqual(releaseRequest.version); + expect(release?.changelogEntry).toEqual(releaseRequest.changelogEntry); + + // Overwrite the release's changelog + const updateReleaseRequest: CliReleaseRequest = { + irVersion: 0, + version: "3.1.2", + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + links: undefined, + upgradeNotes: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, + fixed: undefined, + }, + ], + createdAt: undefined, + isYanked: undefined, + tags: undefined, + }; + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: updateReleaseRequest, + }); + const updatedRelease = await fdrApplication.dao.cliVersions().getCliRelease({ + cliVersion: "3.1.2", + }); + expect(updatedRelease?.irVersion).toEqual(updateReleaseRequest.irVersion); + expect(updatedRelease?.version).toEqual(updateReleaseRequest.version); + expect(updatedRelease?.changelogEntry).toEqual( + updateReleaseRequest.changelogEntry + ); }); it("cli version rc versions", async () => { - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 0, - version: "0.1.2-rc0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); - const oneCli = await fdrApplication.dao.cliVersions().getCliRelease({ - cliVersion: "0.1.2-rc0", - }); - expect(oneCli?.releaseType).toEqual(ReleaseType.Rc); - expect(noncifySemanticVersion("0.1.2-rc0")).toEqual("00000-00001-00002-12-00000"); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 0, + version: "0.1.2-rc0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); + const oneCli = await fdrApplication.dao.cliVersions().getCliRelease({ + cliVersion: "0.1.2-rc0", + }); + expect(oneCli?.releaseType).toEqual(ReleaseType.Rc); + expect(noncifySemanticVersion("0.1.2-rc0")).toEqual( + "00000-00001-00002-12-00000" + ); - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - irVersion: 0, - version: "0.1.2-rc.1", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, - }, - }); - const twoCli = await fdrApplication.dao.cliVersions().getCliRelease({ - cliVersion: "0.1.2-rc.1", - }); - expect(twoCli?.releaseType).toEqual(ReleaseType.Rc); - expect(noncifySemanticVersion("0.1.2-rc.1")).toEqual("00000-00001-00002-12-00001"); + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + irVersion: 0, + version: "0.1.2-rc.1", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); + const twoCli = await fdrApplication.dao.cliVersions().getCliRelease({ + cliVersion: "0.1.2-rc.1", + }); + expect(twoCli?.releaseType).toEqual(ReleaseType.Rc); + expect(noncifySemanticVersion("0.1.2-rc.1")).toEqual( + "00000-00001-00002-12-00001" + ); }); diff --git a/servers/fdr/src/__test__/local/db/generatorDao.test.ts b/servers/fdr/src/__test__/local/db/generatorDao.test.ts index 28e585a0ff..b28d97d782 100644 --- a/servers/fdr/src/__test__/local/db/generatorDao.test.ts +++ b/servers/fdr/src/__test__/local/db/generatorDao.test.ts @@ -3,240 +3,250 @@ import { Generator } from "../../../api/generated/api/resources/generators"; import { createMockFdrApplication } from "../../mock"; const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], + orgIds: ["acme", "octoai"], }); // I didn't make a delete all endpoint because that felt like a bad idea // so we just have to clean up after ourselves with this list of used names. const GENERATORS_FROM_OTHER_TESTS = [ - "this-fails-semver", - "this-picks-latest", - "this-gets-changelog", - "this-is-the-happy-path", - "this-is-cli-restricted", - "python-sdk", - "python-sdk-2", - "python-sdk-3", - "my-cool/example", - "this-is-major-version-restricted", + "this-fails-semver", + "this-picks-latest", + "this-gets-changelog", + "this-is-the-happy-path", + "this-is-cli-restricted", + "python-sdk", + "python-sdk-2", + "python-sdk-3", + "my-cool/example", + "this-is-major-version-restricted", ]; beforeEach(async () => { - // Clean slate - await fdrApplication.dao.generators().deleteGenerators({ generatorIds: GENERATORS_FROM_OTHER_TESTS }); + // Clean slate + await fdrApplication.dao + .generators() + .deleteGenerators({ generatorIds: GENERATORS_FROM_OTHER_TESTS }); }); it("generator dao", async () => { - // create snippets - const generatorStarter: FdrAPI.generators.Generator = { - id: FdrAPI.generators.GeneratorId("my-cool/example"), - displayName: "My Cool Example", - generatorType: { type: "sdk" }, - dockerImage: "my-cool/example", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, - }; - await fdrApplication.dao.generators().upsertGenerator({ - generator: generatorStarter, - }); + // create snippets + const generatorStarter: FdrAPI.generators.Generator = { + id: FdrAPI.generators.GeneratorId("my-cool/example"), + displayName: "My Cool Example", + generatorType: { type: "sdk" }, + dockerImage: "my-cool/example", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], + }, + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }; + await fdrApplication.dao.generators().upsertGenerator({ + generator: generatorStarter, + }); - const generator = await fdrApplication.dao - .generators() - .getGenerator({ generatorId: FdrAPI.generators.GeneratorId("my-cool/example") }); - expect(generator).toEqual(generatorStarter); + const generator = await fdrApplication.dao + .generators() + .getGenerator({ + generatorId: FdrAPI.generators.GeneratorId("my-cool/example"), + }); + expect(generator).toEqual(generatorStarter); - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("my-cool/example"), - generatorType: { type: "sdk" }, - displayName: "My Cool Example", - dockerImage: "changing things up", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Typescript, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("my-cool/example"), + generatorType: { type: "sdk" }, + displayName: "My Cool Example", + dockerImage: "changing things up", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Typescript, + scripts: { + preInstallScript: { + steps: [], }, - }); - const generatorUpdated = await fdrApplication.dao.generators().listGenerators(); - expect(generatorUpdated).length(1); - expect(generatorUpdated[0]).toEqual({ - id: FdrAPI.generators.GeneratorId("my-cool/example"), - generatorType: { type: "sdk" }, - displayName: "My Cool Example", - dockerImage: "changing things up", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Typescript, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, + installScript: { + steps: [], }, - }); + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }, + }); + const generatorUpdated = await fdrApplication.dao + .generators() + .listGenerators(); + expect(generatorUpdated).length(1); + expect(generatorUpdated[0]).toEqual({ + id: FdrAPI.generators.GeneratorId("my-cool/example"), + generatorType: { type: "sdk" }, + displayName: "My Cool Example", + dockerImage: "changing things up", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Typescript, + scripts: { + preInstallScript: { + steps: [], + }, + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }); }); it("generator dao non-unique", async () => { - // essentially just adding a test to make sure we don't apply a uniqueness constraint willy nilly - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("python-sdk"), - displayName: "Python SDK", - generatorType: { type: "sdk" }, - dockerImage: "my-cool/example", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + // essentially just adding a test to make sure we don't apply a uniqueness constraint willy nilly + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("python-sdk"), + displayName: "Python SDK", + generatorType: { type: "sdk" }, + dockerImage: "my-cool/example", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }, + }); - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("python-sdk-2"), - displayName: "Python SDK", - generatorType: { type: "sdk" }, - dockerImage: "my-cool/example-1", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("python-sdk-2"), + displayName: "Python SDK", + generatorType: { type: "sdk" }, + dockerImage: "my-cool/example-1", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }, + }); - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("python-sdk-3"), - displayName: "Python SDK", - generatorType: { type: "sdk" }, - dockerImage: "my-cool/example-2", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("python-sdk-3"), + displayName: "Python SDK", + generatorType: { type: "sdk" }, + dockerImage: "my-cool/example-2", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }, + }); - const generatorUpdated = await fdrApplication.dao.generators().listGenerators(); - expect(generatorUpdated).length(3); + const generatorUpdated = await fdrApplication.dao + .generators() + .listGenerators(); + expect(generatorUpdated).length(3); }); it("generator dao image non-unique", async () => { - const generator: Generator = { - id: FdrAPI.generators.GeneratorId("python-sdk-3"), + const generator: Generator = { + id: FdrAPI.generators.GeneratorId("python-sdk-3"), + displayName: "Python SDK", + generatorType: { type: "sdk" }, + dockerImage: "my-cool/example", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: ["here I am! a step!"], + }, + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }; + await fdrApplication.dao.generators().upsertGenerator({ generator }); + + await expect(async () => { + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("python-sdk-15"), displayName: "Python SDK", generatorType: { type: "sdk" }, dockerImage: "my-cool/example", generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, scripts: { - preInstallScript: { - steps: ["here I am! a step!"], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, - }; - await fdrApplication.dao.generators().upsertGenerator({ generator }); - - await expect(async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("python-sdk-15"), - displayName: "Python SDK", - generatorType: { type: "sdk" }, - dockerImage: "my-cool/example", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, - }, - }); - }).rejects.toThrowError( - "\nInvalid `prisma.generator.upsert()` invocation:\n\n\nUnique constraint failed on the fields: (`dockerImage`)", - ); + preInstallScript: { + steps: [], + }, + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }, + }); + }).rejects.toThrowError( + "\nInvalid `prisma.generator.upsert()` invocation:\n\n\nUnique constraint failed on the fields: (`dockerImage`)" + ); - const generatorByImage = await fdrApplication.dao.generators().getGeneratorByImage({ image: "my-cool/example" }); + const generatorByImage = await fdrApplication.dao + .generators() + .getGeneratorByImage({ image: "my-cool/example" }); - expect(generatorByImage).toEqual(generator); + expect(generatorByImage).toEqual(generator); }); diff --git a/servers/fdr/src/__test__/local/db/generatorVersionsDao.test.ts b/servers/fdr/src/__test__/local/db/generatorVersionsDao.test.ts index 947636c2bd..f8e73df0cd 100644 --- a/servers/fdr/src/__test__/local/db/generatorVersionsDao.test.ts +++ b/servers/fdr/src/__test__/local/db/generatorVersionsDao.test.ts @@ -3,719 +3,751 @@ import { InvalidVersionError } from "../../../api/generated/api/resources/genera import { createMockFdrApplication } from "../../mock"; const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], + orgIds: ["acme", "octoai"], }); function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } // Bad version on create throws it("generator version dao not semver", async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("this-fails-semver"), - displayName: "An SDK", - generatorType: { type: "sdk" }, - dockerImage: "this-fails-semver", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("this-fails-semver"), + displayName: "An SDK", + generatorType: { type: "sdk" }, + dockerImage: "this-fails-semver", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); + installScript: { + steps: [], + }, + compileScript: { + steps: [], + }, + testScript: { + steps: [], + }, + }, + }, + }); - await expect(async () => { - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-fails-semver"), - irVersion: 0, - version: "abc.1.2", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }, - }); - }).rejects.toThrow(new InvalidVersionError({ providedVersion: "abc.1.2" })); + await expect(async () => { + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-fails-semver"), + irVersion: 0, + version: "abc.1.2", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + }).rejects.toThrow(new InvalidVersionError({ providedVersion: "abc.1.2" })); }); // Insert multiple and return the right latest (insert out of order) it("generator version get latest respects semver, not time", async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("this-picks-latest"), - displayName: "An SDK", - generatorType: { type: "sdk" }, - dockerImage: "this-picks-latest", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("this-picks-latest"), + displayName: "An SDK", + generatorType: { type: "sdk" }, + dockerImage: "this-picks-latest", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); - // create some versions and sleep between them to ensure the timestamps are different - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), - irVersion: 2, - version: "0.1.2", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + installScript: { + steps: [], }, - }); - - await delay(1000); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), - irVersion: 0, - version: "1.1.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + compileScript: { + steps: [], }, - }); - - await delay(1000); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), - irVersion: 0, - version: "0.1.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + testScript: { + steps: [], }, - }); - - // update an old one too to impact update time - await delay(1000); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), - irVersion: 5, - version: "0.1.2", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + }, + }, + }); + // create some versions and sleep between them to ensure the timestamps are different + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), + irVersion: 2, + version: "0.1.2", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await delay(1000); + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), + irVersion: 0, + version: "1.1.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await delay(1000); + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), + irVersion: 0, + version: "0.1.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + // update an old one too to impact update time + await delay(1000); + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-picks-latest"), + irVersion: 5, + version: "0.1.2", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + // Get latest and make sure it's 1.1.0 + expect( + ( + await fdrApplication.dao.generatorVersions().getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest: { + generator: FdrAPI.generators.GeneratorId("this-picks-latest"), }, - }); - - // Get latest and make sure it's 1.1.0 - expect( - ( - await fdrApplication.dao.generatorVersions().getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest: { generator: FdrAPI.generators.GeneratorId("this-picks-latest") }, - }) - )?.version, - ).toEqual("1.1.0"); + }) + )?.version + ).toEqual("1.1.0"); }); // Get changelog it("generator changelog", async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("this-gets-changelog"), - displayName: "An SDK", - generatorType: { type: "sdk" }, - dockerImage: "this-gets-changelog", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("this-gets-changelog"), + displayName: "An SDK", + generatorType: { type: "sdk" }, + dockerImage: "this-gets-changelog", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); - - // create some versions and sleep between them to ensure the timestamps are different - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), - irVersion: 0, - version: "2.1.2", - changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - links: undefined, - upgradeNotes: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - fixed: undefined, - }, - ], - createdAt: undefined, - isYanked: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + installScript: { + steps: [], }, - }); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), - irVersion: 0, - version: "2.1.3", - changelogEntry: [ - { - type: "fix", - summary: "fixed that new feature", - fixed: ["fixed that new feature"], - links: undefined, - upgradeNotes: undefined, - added: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - }, - ], - createdAt: undefined, - isYanked: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + compileScript: { + steps: [], }, - }); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), - irVersion: 0, - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - links: undefined, - upgradeNotes: undefined, - added: undefined, - changed: undefined, - removed: undefined, - }, - ], - createdAt: undefined, - isYanked: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + testScript: { + steps: [], }, - }); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), - irVersion: 0, - version: "2.1.6", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + }, + }, + }); + + // create some versions and sleep between them to ensure the timestamps are different + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), + irVersion: 0, + version: "2.1.2", + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + links: undefined, + upgradeNotes: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, + fixed: undefined, }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), - irVersion: 0, - version: "2.1.8", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + ], + createdAt: undefined, + isYanked: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), + irVersion: 0, + version: "2.1.3", + changelogEntry: [ + { + type: "fix", + summary: "fixed that new feature", + fixed: ["fixed that new feature"], + links: undefined, + upgradeNotes: undefined, + added: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, }, - }); - - // Note we explicitly do not include 0.1.2 and 0.1.8 in the range to ensure we're only including the range - expect( - await fdrApplication.dao.generatorVersions().getChangelog({ - generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), - versionRanges: { - fromVersion: { type: "inclusive", value: "2.1.3" }, - toVersion: { type: "inclusive", value: "2.1.7" }, - }, - }), - ).toEqual({ - entries: [ - { - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - }, - ], - }, - { - version: "2.1.3", - changelogEntry: [ - { - type: "fix", - summary: "fixed that new feature", - fixed: ["fixed that new feature"], - }, - ], - }, + ], + createdAt: undefined, + isYanked: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), + irVersion: 0, + version: "2.1.5", + changelogEntry: [ + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + links: undefined, + upgradeNotes: undefined, + added: undefined, + changed: undefined, + removed: undefined, + }, + ], + createdAt: undefined, + isYanked: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), + irVersion: 0, + version: "2.1.6", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-gets-changelog"), + irVersion: 0, + version: "2.1.8", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + // Note we explicitly do not include 0.1.2 and 0.1.8 in the range to ensure we're only including the range + expect( + await fdrApplication.dao.generatorVersions().getChangelog({ + generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), + versionRanges: { + fromVersion: { type: "inclusive", value: "2.1.3" }, + toVersion: { type: "inclusive", value: "2.1.7" }, + }, + }) + ).toEqual({ + entries: [ + { + version: "2.1.5", + changelogEntry: [ + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + }, ], - }); - - // Should not get the minimum, given it's exclusive - expect( - await fdrApplication.dao.generatorVersions().getChangelog({ - generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), - versionRanges: { - fromVersion: { type: "exclusive", value: "2.1.3" }, - toVersion: { type: "exclusive", value: "2.1.7" }, - }, - }), - ).toEqual({ - entries: [ - { - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - }, - ], - }, + }, + { + version: "2.1.3", + changelogEntry: [ + { + type: "fix", + summary: "fixed that new feature", + fixed: ["fixed that new feature"], + }, ], - }); - - // Should get every changelog - expect( - await fdrApplication.dao.generatorVersions().getChangelog({ - generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), - versionRanges: { - fromVersion: { type: "inclusive", value: "2.1.2" }, - toVersion: { type: "inclusive", value: "2.1.8" }, - }, - }), - ).toEqual({ - entries: [ - { - version: "2.1.5", - changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - }, - ], - }, - { - version: "2.1.3", - changelogEntry: [ - { - type: "fix", - summary: "fixed that new feature", - fixed: ["fixed that new feature"], - }, - ], - }, - { - version: "2.1.2", - changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - }, - ], - }, + }, + ], + }); + + // Should not get the minimum, given it's exclusive + expect( + await fdrApplication.dao.generatorVersions().getChangelog({ + generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), + versionRanges: { + fromVersion: { type: "exclusive", value: "2.1.3" }, + toVersion: { type: "exclusive", value: "2.1.7" }, + }, + }) + ).toEqual({ + entries: [ + { + version: "2.1.5", + changelogEntry: [ + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + }, ], - }); -}); -// Update version -it("generator version happy path update", async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), - displayName: "An SDK", - generatorType: { type: "sdk" }, - dockerImage: "this-is-the-happy-path", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, - }, - }); - - const releaseRequest: FdrAPI.generators.GeneratorReleaseRequest = { - generatorId: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), - irVersion: 2, - version: "3.1.2", + }, + ], + }); + + // Should get every changelog + expect( + await fdrApplication.dao.generatorVersions().getChangelog({ + generator: FdrAPI.generators.GeneratorId("this-gets-changelog"), + versionRanges: { + fromVersion: { type: "inclusive", value: "2.1.2" }, + toVersion: { type: "inclusive", value: "2.1.8" }, + }, + }) + ).toEqual({ + entries: [ + { + version: "2.1.5", changelogEntry: [ - { - type: "fix", - summary: "did a couple things", - fixed: ["fixed that new feature"], - deprecated: ["idk google meet or something isn't there anymore"], - links: undefined, - upgradeNotes: undefined, - added: undefined, - changed: undefined, - removed: undefined, - }, + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + }, ], - createdAt: undefined, - isYanked: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }; - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: releaseRequest, - }); - const release = await fdrApplication.dao.generatorVersions().getGeneratorRelease({ - generator: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), - version: "3.1.2", - }); - expect(release?.generatorId).toEqual(releaseRequest.generatorId); - expect(release?.irVersion).toEqual(releaseRequest.irVersion); - expect(release?.version).toEqual(releaseRequest.version); - expect(release?.changelogEntry).toEqual(releaseRequest.changelogEntry); - - // Overwrite the release's changelog - const updateReleaseRequest: FdrAPI.generators.GeneratorReleaseRequest = { - generatorId: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), - irVersion: 2, - version: "3.1.2", + }, + { + version: "2.1.3", changelogEntry: [ - { - type: "feat", - summary: "added a new feature", - added: ["added a new feature"], - links: undefined, - upgradeNotes: undefined, - changed: undefined, - deprecated: undefined, - removed: undefined, - fixed: undefined, - }, + { + type: "fix", + summary: "fixed that new feature", + fixed: ["fixed that new feature"], + }, ], - createdAt: undefined, - isYanked: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }; - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: updateReleaseRequest, - }); - const updatedRelease = await fdrApplication.dao.generatorVersions().getGeneratorRelease({ - generator: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), - version: "3.1.2", - }); - expect(updatedRelease?.generatorId).toEqual(updateReleaseRequest.generatorId); - expect(updatedRelease?.irVersion).toEqual(updateReleaseRequest.irVersion); - expect(updatedRelease?.version).toEqual(updateReleaseRequest.version); - expect(updatedRelease?.changelogEntry).toEqual(updateReleaseRequest.changelogEntry); + }, + { + version: "2.1.2", + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + }, + ], + }, + ], + }); }); - -it("get generator that works for cli version", async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - displayName: "An SDK", - generatorType: { type: "sdk" }, - dockerImage: "this-is-cli-restricted", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, +// Update version +it("generator version happy path update", async () => { + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), + displayName: "An SDK", + generatorType: { type: "sdk" }, + dockerImage: "this-is-the-happy-path", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); - - await fdrApplication.dao.cliVersions().upsertCliRelease({ - cliRelease: { - version: "0.100.0", - irVersion: 52, - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - tags: undefined, + installScript: { + steps: [], }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - irVersion: 50, - version: "2.1.8", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + compileScript: { + steps: [], }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - irVersion: 51, - version: "3.0.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + testScript: { + steps: [], }, - }); + }, + }, + }); + + const releaseRequest: FdrAPI.generators.GeneratorReleaseRequest = { + generatorId: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), + irVersion: 2, + version: "3.1.2", + changelogEntry: [ + { + type: "fix", + summary: "did a couple things", + fixed: ["fixed that new feature"], + deprecated: ["idk google meet or something isn't there anymore"], + links: undefined, + upgradeNotes: undefined, + added: undefined, + changed: undefined, + removed: undefined, + }, + ], + createdAt: undefined, + isYanked: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }; + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: releaseRequest, + }); + const release = await fdrApplication.dao + .generatorVersions() + .getGeneratorRelease({ + generator: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), + version: "3.1.2", + }); + expect(release?.generatorId).toEqual(releaseRequest.generatorId); + expect(release?.irVersion).toEqual(releaseRequest.irVersion); + expect(release?.version).toEqual(releaseRequest.version); + expect(release?.changelogEntry).toEqual(releaseRequest.changelogEntry); + + // Overwrite the release's changelog + const updateReleaseRequest: FdrAPI.generators.GeneratorReleaseRequest = { + generatorId: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), + irVersion: 2, + version: "3.1.2", + changelogEntry: [ + { + type: "feat", + summary: "added a new feature", + added: ["added a new feature"], + links: undefined, + upgradeNotes: undefined, + changed: undefined, + deprecated: undefined, + removed: undefined, + fixed: undefined, + }, + ], + createdAt: undefined, + isYanked: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }; + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: updateReleaseRequest, + }); + const updatedRelease = await fdrApplication.dao + .generatorVersions() + .getGeneratorRelease({ + generator: FdrAPI.generators.GeneratorId("this-is-the-happy-path"), + version: "3.1.2", + }); + expect(updatedRelease?.generatorId).toEqual(updateReleaseRequest.generatorId); + expect(updatedRelease?.irVersion).toEqual(updateReleaseRequest.irVersion); + expect(updatedRelease?.version).toEqual(updateReleaseRequest.version); + expect(updatedRelease?.changelogEntry).toEqual( + updateReleaseRequest.changelogEntry + ); +}); - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - irVersion: 51, - version: "3.1.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, +it("get generator that works for cli version", async () => { + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + displayName: "An SDK", + generatorType: { type: "sdk" }, + dockerImage: "this-is-cli-restricted", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - irVersion: 52, - version: "3.5.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + installScript: { + steps: [], }, - }); - - // Get with retain major at 2 - const releaseRetainMajor = await fdrApplication.dao.generatorVersions().getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest: { - generator: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - cliVersion: "0.100.0", - generatorMajorVersion: 2, + compileScript: { + steps: [], }, - }); - expect(releaseRetainMajor?.version).toEqual("2.1.8"); - - const release = await fdrApplication.dao.generatorVersions().getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest: { - generator: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), - cliVersion: "0.100.0", + testScript: { + steps: [], }, - }); - expect(release?.version).toEqual("3.5.0"); + }, + }, + }); + + await fdrApplication.dao.cliVersions().upsertCliRelease({ + cliRelease: { + version: "0.100.0", + irVersion: 52, + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + irVersion: 50, + version: "2.1.8", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + irVersion: 51, + version: "3.0.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + irVersion: 51, + version: "3.1.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + irVersion: 52, + version: "3.5.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + // Get with retain major at 2 + const releaseRetainMajor = await fdrApplication.dao + .generatorVersions() + .getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest: { + generator: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + cliVersion: "0.100.0", + generatorMajorVersion: 2, + }, + }); + expect(releaseRetainMajor?.version).toEqual("2.1.8"); + + const release = await fdrApplication.dao + .generatorVersions() + .getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest: { + generator: FdrAPI.generators.GeneratorId("this-is-cli-restricted"), + cliVersion: "0.100.0", + }, + }); + expect(release?.version).toEqual("3.5.0"); }); it("get generator retain major version", async () => { - await fdrApplication.dao.generators().upsertGenerator({ - generator: { - id: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - displayName: "An SDK", - generatorType: { type: "sdk" }, - dockerImage: "this-is-major-version-restricted", - generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, - scripts: { - preInstallScript: { - steps: [], - }, - installScript: { - steps: [], - }, - compileScript: { - steps: [], - }, - testScript: { - steps: [], - }, - }, - }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - irVersion: 50, - version: "2.0.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - irVersion: 50, - version: "2.1.0-rc0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - irVersion: 50, - version: "2.1.8", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - irVersion: 51, - version: "3.0.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, - }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - irVersion: 51, - version: "3.1.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + await fdrApplication.dao.generators().upsertGenerator({ + generator: { + id: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), + displayName: "An SDK", + generatorType: { type: "sdk" }, + dockerImage: "this-is-major-version-restricted", + generatorLanguage: FdrAPI.generators.GeneratorLanguage.Python, + scripts: { + preInstallScript: { + steps: [], }, - }); - - await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ - generatorRelease: { - generatorId: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - irVersion: 52, - version: "3.5.0", - createdAt: undefined, - isYanked: undefined, - changelogEntry: undefined, - migration: undefined, - customConfigSchema: undefined, - tags: undefined, + installScript: { + steps: [], }, - }); - - // Get with retain major at 2 - const releaseRetainMajor = await fdrApplication.dao.generatorVersions().getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest: { - generator: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - releaseTypes: ["GA"], - generatorMajorVersion: 2, + compileScript: { + steps: [], }, - }); - expect(releaseRetainMajor?.version).toEqual("2.1.8"); - - const release = await fdrApplication.dao.generatorVersions().getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest: { - generator: FdrAPI.generators.GeneratorId("this-is-major-version-restricted"), - releaseTypes: ["GA"], + testScript: { + steps: [], }, - }); - expect(release?.version).toEqual("3.5.0"); + }, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + irVersion: 50, + version: "2.0.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + irVersion: 50, + version: "2.1.0-rc0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + irVersion: 50, + version: "2.1.8", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + irVersion: 51, + version: "3.0.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + irVersion: 51, + version: "3.1.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + await fdrApplication.dao.generatorVersions().upsertGeneratorRelease({ + generatorRelease: { + generatorId: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + irVersion: 52, + version: "3.5.0", + createdAt: undefined, + isYanked: undefined, + changelogEntry: undefined, + migration: undefined, + customConfigSchema: undefined, + tags: undefined, + }, + }); + + // Get with retain major at 2 + const releaseRetainMajor = await fdrApplication.dao + .generatorVersions() + .getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest: { + generator: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + releaseTypes: ["GA"], + generatorMajorVersion: 2, + }, + }); + expect(releaseRetainMajor?.version).toEqual("2.1.8"); + + const release = await fdrApplication.dao + .generatorVersions() + .getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest: { + generator: FdrAPI.generators.GeneratorId( + "this-is-major-version-restricted" + ), + releaseTypes: ["GA"], + }, + }); + expect(release?.version).toEqual("3.5.0"); }); diff --git a/servers/fdr/src/__test__/local/db/gitDao.test.ts b/servers/fdr/src/__test__/local/db/gitDao.test.ts index 4506fdd55f..4ed3316f7a 100644 --- a/servers/fdr/src/__test__/local/db/gitDao.test.ts +++ b/servers/fdr/src/__test__/local/db/gitDao.test.ts @@ -3,154 +3,158 @@ import { PullRequest, PullRequestState } from "../../../api/generated/api"; import { createMockFdrApplication } from "../../mock"; const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], + orgIds: ["acme", "octoai"], }); it("repo happy path", async () => { - const repo1: FdrAPI.FernRepository = { - type: "sdk", - id: { type: "github", id: "acme-123" }, - name: "repo1", - owner: "acme", - fullName: "acme/repo1", - repositoryOwnerOrganizationId: FdrAPI.OrgId("acme"), - url: FdrAPI.Url("https://123.com"), - defaultBranchChecks: [], - sdkLanguage: "python", - }; - await fdrApplication.dao.git().upsertRepository({ repository: repo1 }); + const repo1: FdrAPI.FernRepository = { + type: "sdk", + id: { type: "github", id: "acme-123" }, + name: "repo1", + owner: "acme", + fullName: "acme/repo1", + repositoryOwnerOrganizationId: FdrAPI.OrgId("acme"), + url: FdrAPI.Url("https://123.com"), + defaultBranchChecks: [], + sdkLanguage: "python", + }; + await fdrApplication.dao.git().upsertRepository({ repository: repo1 }); - const repo2: FdrAPI.FernRepository = { - type: "config", - id: { type: "github", id: "octo-123" }, - name: "repo1", - owner: "octoai", - fullName: "octoai/repo1", - repositoryOwnerOrganizationId: FdrAPI.OrgId("octoai"), - url: FdrAPI.Url("https://123.com"), - defaultBranchChecks: [], - }; - await fdrApplication.dao.git().upsertRepository({ repository: repo2 }); + const repo2: FdrAPI.FernRepository = { + type: "config", + id: { type: "github", id: "octo-123" }, + name: "repo1", + owner: "octoai", + fullName: "octoai/repo1", + repositoryOwnerOrganizationId: FdrAPI.OrgId("octoai"), + url: FdrAPI.Url("https://123.com"), + defaultBranchChecks: [], + }; + await fdrApplication.dao.git().upsertRepository({ repository: repo2 }); - const repos = await fdrApplication.dao.git().listRepository({ - repositoryName: undefined, - repositoryOwner: undefined, - organizationId: undefined, - }); - expect(repos.repositories).toEqual([repo1, repo2]); + const repos = await fdrApplication.dao.git().listRepository({ + repositoryName: undefined, + repositoryOwner: undefined, + organizationId: undefined, + }); + expect(repos.repositories).toEqual([repo1, repo2]); - const repo1FromDb = await fdrApplication.dao - .git() - .getRepository({ repositoryOwner: "acme", repositoryName: "repo1" }); - expect(repo1FromDb).toEqual(repo1); + const repo1FromDb = await fdrApplication.dao + .git() + .getRepository({ repositoryOwner: "acme", repositoryName: "repo1" }); + expect(repo1FromDb).toEqual(repo1); }); it("pulls happy path", async () => { - const repository: FdrAPI.FernRepository = { - type: "sdk", - id: { type: "github", id: "12345" }, - name: "repoForPRs", - owner: "acme", - fullName: "acme/repoForPRs", - repositoryOwnerOrganizationId: FdrAPI.OrgId("acme"), - url: FdrAPI.Url("https://123.com"), - defaultBranchChecks: [], - sdkLanguage: "python", - }; - await fdrApplication.dao.git().upsertRepository({ repository }); + const repository: FdrAPI.FernRepository = { + type: "sdk", + id: { type: "github", id: "12345" }, + name: "repoForPRs", + owner: "acme", + fullName: "acme/repoForPRs", + repositoryOwnerOrganizationId: FdrAPI.OrgId("acme"), + url: FdrAPI.Url("https://123.com"), + defaultBranchChecks: [], + sdkLanguage: "python", + }; + await fdrApplication.dao.git().upsertRepository({ repository }); + + const pull1: PullRequest = { + pullRequestNumber: 1, + repositoryOwner: "acme", + repositoryName: "repoForPRs", + author: { + name: "Not Armando", + email: "armando@buildwithfern.com", + username: "not_armando", + }, + reviewers: [], + title: "PR 1", + url: FdrAPI.Url("https://123.com"), + checks: [], + state: PullRequestState.Open, + createdAt: new Date().toISOString(), + updatedAt: undefined, + mergedAt: undefined, + closedAt: undefined, + }; + await fdrApplication.dao.git().upsertPullRequest({ pullRequest: pull1 }); - const pull1: PullRequest = { - pullRequestNumber: 1, - repositoryOwner: "acme", - repositoryName: "repoForPRs", - author: { - name: "Not Armando", - email: "armando@buildwithfern.com", - username: "not_armando", - }, - reviewers: [], - title: "PR 1", - url: FdrAPI.Url("https://123.com"), - checks: [], - state: PullRequestState.Open, - createdAt: new Date().toISOString(), - updatedAt: undefined, - mergedAt: undefined, - closedAt: undefined, - }; - await fdrApplication.dao.git().upsertPullRequest({ pullRequest: pull1 }); + const pull2: PullRequest = { + pullRequestNumber: 2, + repositoryOwner: "acme", + repositoryName: "repoForPRs", + author: { + name: "Armando", + email: "armando@buildwithfern.com", + username: "armando", + }, + reviewers: [], + title: "PR 2", + url: FdrAPI.Url("https://123.com"), + checks: [], + state: PullRequestState.Merged, + createdAt: new Date().toISOString(), + mergedAt: new Date().toISOString(), + updatedAt: undefined, + closedAt: undefined, + }; + await fdrApplication.dao.git().upsertPullRequest({ pullRequest: pull2 }); - const pull2: PullRequest = { - pullRequestNumber: 2, - repositoryOwner: "acme", - repositoryName: "repoForPRs", - author: { - name: "Armando", - email: "armando@buildwithfern.com", - username: "armando", - }, - reviewers: [], - title: "PR 2", - url: FdrAPI.Url("https://123.com"), - checks: [], - state: PullRequestState.Merged, - createdAt: new Date().toISOString(), - mergedAt: new Date().toISOString(), - updatedAt: undefined, - closedAt: undefined, - }; - await fdrApplication.dao.git().upsertPullRequest({ pullRequest: pull2 }); + const pulls = await fdrApplication.dao.git().listPullRequests({ + repositoryName: undefined, + repositoryOwner: undefined, + organizationId: undefined, + state: undefined, + author: undefined, + }); + expect(pulls.pullRequests).toEqual([pull2, pull1]); - const pulls = await fdrApplication.dao.git().listPullRequests({ - repositoryName: undefined, - repositoryOwner: undefined, - organizationId: undefined, - state: undefined, - author: undefined, + const pull1FromDb = await fdrApplication.dao + .git() + .getPullRequest({ + repositoryOwner: "acme", + repositoryName: "repoForPRs", + pullRequestNumber: 1, }); - expect(pulls.pullRequests).toEqual([pull2, pull1]); + expect(pull1FromDb).toEqual(pull1); - const pull1FromDb = await fdrApplication.dao - .git() - .getPullRequest({ repositoryOwner: "acme", repositoryName: "repoForPRs", pullRequestNumber: 1 }); - expect(pull1FromDb).toEqual(pull1); + const pull3: PullRequest = { + pullRequestNumber: 3, + repositoryOwner: "acme", + repositoryName: "repoForPRs", + author: { + name: "Armando", + email: "armando@buildwithfern.com", + username: "armando", + }, + reviewers: [], + title: "PR 2", + url: FdrAPI.Url("https://123.com"), + checks: [], + state: PullRequestState.Merged, + createdAt: new Date().toISOString(), + mergedAt: new Date().toISOString(), + updatedAt: undefined, + closedAt: undefined, + }; + await fdrApplication.dao.git().upsertPullRequest({ pullRequest: pull3 }); - const pull3: PullRequest = { - pullRequestNumber: 3, - repositoryOwner: "acme", - repositoryName: "repoForPRs", - author: { - name: "Armando", - email: "armando@buildwithfern.com", - username: "armando", - }, - reviewers: [], - title: "PR 2", - url: FdrAPI.Url("https://123.com"), - checks: [], - state: PullRequestState.Merged, - createdAt: new Date().toISOString(), - mergedAt: new Date().toISOString(), - updatedAt: undefined, - closedAt: undefined, - }; - await fdrApplication.dao.git().upsertPullRequest({ pullRequest: pull3 }); + const pullsByState = await fdrApplication.dao.git().listPullRequests({ + repositoryName: undefined, + repositoryOwner: undefined, + organizationId: undefined, + state: [PullRequestState.Merged], + author: undefined, + }); + expect(pullsByState.pullRequests).toEqual([pull3, pull2]); - const pullsByState = await fdrApplication.dao.git().listPullRequests({ - repositoryName: undefined, - repositoryOwner: undefined, - organizationId: undefined, - state: [PullRequestState.Merged], - author: undefined, - }); - expect(pullsByState.pullRequests).toEqual([pull3, pull2]); - - const pullsByAuthor = await fdrApplication.dao.git().listPullRequests({ - repositoryName: undefined, - repositoryOwner: undefined, - organizationId: undefined, - state: undefined, - author: ["not_armando"], - }); - expect(pullsByAuthor.pullRequests).toEqual([pull1]); + const pullsByAuthor = await fdrApplication.dao.git().listPullRequests({ + repositoryName: undefined, + repositoryOwner: undefined, + organizationId: undefined, + state: undefined, + author: ["not_armando"], + }); + expect(pullsByAuthor.pullRequests).toEqual([pull1]); }); diff --git a/servers/fdr/src/__test__/local/db/snippetsDao.test.ts b/servers/fdr/src/__test__/local/db/snippetsDao.test.ts index b1cf75f761..2bf99c043d 100644 --- a/servers/fdr/src/__test__/local/db/snippetsDao.test.ts +++ b/servers/fdr/src/__test__/local/db/snippetsDao.test.ts @@ -2,593 +2,625 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { createMockFdrApplication } from "../../mock"; const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], + orgIds: ["acme", "octoai"], }); it("snippet api dao", async () => { - // create snippets - await fdrApplication.dao.snippets().storeSnippets({ - storeSnippetsInfo: { - orgId: FdrAPI.OrgId("example"), - apiId: FdrAPI.ApiId("bar"), - sdk: { - type: "python", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - async_client: "invalid", - sync_client: "invalid", - }, - exampleIdentifier: undefined, - }, - ], - }, - }, - }); - // get snippets with orgId - const snippetAPIs = await fdrApplication.dao.snippetAPIs().loadSnippetAPIs({ - loadSnippetAPIsRequest: { - orgIds: [FdrAPI.OrgId("example"), FdrAPI.OrgId("fern")], - apiName: undefined, + // create snippets + await fdrApplication.dao.snippets().storeSnippets({ + storeSnippetsInfo: { + orgId: FdrAPI.OrgId("example"), + apiId: FdrAPI.ApiId("bar"), + sdk: { + type: "python", + sdk: { + package: "acme", + version: "0.0.1", }, - }); - expect(snippetAPIs).not.toEqual(undefined); - expect(snippetAPIs?.length).toEqual(1); + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + async_client: "invalid", + sync_client: "invalid", + }, + exampleIdentifier: undefined, + }, + ], + }, + }, + }); + // get snippets with orgId + const snippetAPIs = await fdrApplication.dao.snippetAPIs().loadSnippetAPIs({ + loadSnippetAPIsRequest: { + orgIds: [FdrAPI.OrgId("example"), FdrAPI.OrgId("fern")], + apiName: undefined, + }, + }); + expect(snippetAPIs).not.toEqual(undefined); + expect(snippetAPIs?.length).toEqual(1); - const snippetAPI = snippetAPIs?.[0]; - expect(snippetAPI).not.toEqual(undefined); - expect(snippetAPI?.orgId).toEqual("example"); - expect(snippetAPI?.apiName).toEqual("bar"); + const snippetAPI = snippetAPIs?.[0]; + expect(snippetAPI).not.toEqual(undefined); + expect(snippetAPI?.orgId).toEqual("example"); + expect(snippetAPI?.apiName).toEqual("bar"); - // get snippets with orgId and apiId - const sameSnippetAPIs = await fdrApplication.dao.snippetAPIs().loadSnippetAPIs({ - loadSnippetAPIsRequest: { - orgIds: [FdrAPI.OrgId("example"), FdrAPI.OrgId("fern")], - apiName: "bar", - }, + // get snippets with orgId and apiId + const sameSnippetAPIs = await fdrApplication.dao + .snippetAPIs() + .loadSnippetAPIs({ + loadSnippetAPIsRequest: { + orgIds: [FdrAPI.OrgId("example"), FdrAPI.OrgId("fern")], + apiName: "bar", + }, }); - expect(sameSnippetAPIs).not.toEqual(undefined); - expect(sameSnippetAPIs?.length).toEqual(1); + expect(sameSnippetAPIs).not.toEqual(undefined); + expect(sameSnippetAPIs?.length).toEqual(1); - const sameSnippetAPI = snippetAPIs?.[0]; - expect(sameSnippetAPI).not.toEqual(undefined); - expect(sameSnippetAPI?.orgId).toEqual("example"); - expect(sameSnippetAPI?.apiName).toEqual("bar"); + const sameSnippetAPI = snippetAPIs?.[0]; + expect(sameSnippetAPI).not.toEqual(undefined); + expect(sameSnippetAPI?.orgId).toEqual("example"); + expect(sameSnippetAPI?.apiName).toEqual("bar"); }); it("snippets dao", async () => { - // create snippets - await fdrApplication.dao.snippets().storeSnippets({ - storeSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - sdk: { - type: "python", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - async_client: "invalid", - sync_client: "invalid", - }, - exampleIdentifier: undefined, - }, - ], - }, + // create snippets + await fdrApplication.dao.snippets().storeSnippets({ + storeSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + sdk: { + type: "python", + sdk: { + package: "acme", + version: "0.0.1", }, - }); - // overwrite snippets for the same SDK - await fdrApplication.dao.snippets().storeSnippets({ - storeSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - sdk: { - type: "python", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", - sync_client: "client = Acme(api_key='YOUR_API_KEY')", - }, - exampleIdentifier: undefined, - }, - ], + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, }, - }, - }); - await fdrApplication.dao.snippets().storeSnippets({ - storeSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - sdk: { - type: "python", - sdk: { - package: "acme", - version: "0.0.2", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", - sync_client: "client = Acme(api_key='YOUR_API_KEY')", - }, - exampleIdentifier: undefined, - }, - ], + snippet: { + async_client: "invalid", + sync_client: "invalid", }, + exampleIdentifier: undefined, + }, + ], + }, + }, + }); + // overwrite snippets for the same SDK + await fdrApplication.dao.snippets().storeSnippets({ + storeSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + sdk: { + type: "python", + sdk: { + package: "acme", + version: "0.0.1", }, - }); - // get snippets - const response = await fdrApplication.dao.snippets().loadSnippetsPage({ - loadSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - endpointIdentifier: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", + sync_client: "client = Acme(api_key='YOUR_API_KEY')", }, - sdks: undefined, - page: undefined, exampleIdentifier: undefined, + }, + ], + }, + }, + }); + await fdrApplication.dao.snippets().storeSnippets({ + storeSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + sdk: { + type: "python", + sdk: { + package: "acme", + version: "0.0.2", }, - }); - expect(response).not.toEqual(undefined); - console.log("broo", JSON.stringify(response, null, 2)); - expect(Object.keys(response?.snippets ?? {}).length).toEqual(1); - - const snippets = response?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; - if (snippets === undefined) { - throw new Error("snippets were undefined"); - } - expect(snippets).not.toEqual(undefined); - expect(snippets.length).toEqual(2); + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", + sync_client: "client = Acme(api_key='YOUR_API_KEY')", + }, + exampleIdentifier: undefined, + }, + ], + }, + }, + }); + // get snippets + const response = await fdrApplication.dao.snippets().loadSnippetsPage({ + loadSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + endpointIdentifier: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + sdks: undefined, + page: undefined, + exampleIdentifier: undefined, + }, + }); + expect(response).not.toEqual(undefined); + console.log("broo", JSON.stringify(response, null, 2)); + expect(Object.keys(response?.snippets ?? {}).length).toEqual(1); - const snippet = snippets[0]; - if (snippet === undefined) { - throw new Error("snippet was undefined"); - } - expect(snippet).not.toEqual(undefined); - expect(snippet.type).toEqual("python"); + const snippets = + response?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; + if (snippets === undefined) { + throw new Error("snippets were undefined"); + } + expect(snippets).not.toEqual(undefined); + expect(snippets.length).toEqual(2); - if (snippet.type != "python") { - throw new Error("expected a python snippet"); - } - expect(snippet.sdk.package).toEqual("acme"); - expect(snippet.sdk.version).toEqual("0.0.2"); - expect(snippet.async_client).toEqual("client = AsyncAcme(api_key='YOUR_API_KEY')"); - expect(snippet.sync_client).toEqual("client = Acme(api_key='YOUR_API_KEY')"); + const snippet = snippets[0]; + if (snippet === undefined) { + throw new Error("snippet was undefined"); + } + expect(snippet).not.toEqual(undefined); + expect(snippet.type).toEqual("python"); - const sdkId = await fdrApplication.dao.sdks().getSdkIdForPackage({ sdkPackage: "acme", language: "PYTHON" }); - expect(sdkId).toEqual("python|acme|0.0.2"); + if (snippet.type != "python") { + throw new Error("expected a python snippet"); + } + expect(snippet.sdk.package).toEqual("acme"); + expect(snippet.sdk.version).toEqual("0.0.2"); + expect(snippet.async_client).toEqual( + "client = AsyncAcme(api_key='YOUR_API_KEY')" + ); + expect(snippet.sync_client).toEqual("client = Acme(api_key='YOUR_API_KEY')"); - const sdkIdPrevious = await fdrApplication.dao - .sdks() - .getSdkIdForPackage({ sdkPackage: "acme", language: "PYTHON", version: "0.0.1" }); - expect(sdkIdPrevious).toEqual("python|acme|0.0.1"); + const sdkId = await fdrApplication.dao + .sdks() + .getSdkIdForPackage({ sdkPackage: "acme", language: "PYTHON" }); + expect(sdkId).toEqual("python|acme|0.0.2"); - const snippetsForSdkId = await fdrApplication.dao.snippets().loadAllSnippetsForSdkIds(sdkId != null ? [sdkId] : []); - expect(snippetsForSdkId).toEqual({ - "python|acme|0.0.2": { - "/users/v1": { - DELETE: [], - GET: [ - { - async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", - sdk: { package: "acme", version: "0.0.2" }, - sync_client: "client = Acme(api_key='YOUR_API_KEY')", - type: "python", - }, - ], - PATCH: [], - POST: [], - PUT: [], - }, - }, + const sdkIdPrevious = await fdrApplication.dao + .sdks() + .getSdkIdForPackage({ + sdkPackage: "acme", + language: "PYTHON", + version: "0.0.1", }); + expect(sdkIdPrevious).toEqual("python|acme|0.0.1"); + + const snippetsForSdkId = await fdrApplication.dao + .snippets() + .loadAllSnippetsForSdkIds(sdkId != null ? [sdkId] : []); + expect(snippetsForSdkId).toEqual({ + "python|acme|0.0.2": { + "/users/v1": { + DELETE: [], + GET: [ + { + async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", + sdk: { package: "acme", version: "0.0.2" }, + sync_client: "client = Acme(api_key='YOUR_API_KEY')", + type: "python", + }, + ], + PATCH: [], + POST: [], + PUT: [], + }, + }, + }); }); it("snippets dao with example id", async () => { - // create snippets - await fdrApplication.dao.snippets().storeSnippets({ - storeSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("apiId"), - sdk: { - type: "python", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - async_client: "invalid", - sync_client: "invalid", - }, - exampleIdentifier: "example1", - }, - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", - sync_client: "client = Acme(api_key='YOUR_API_KEY')", - }, - exampleIdentifier: "example2", - }, - ], - }, + // create snippets + await fdrApplication.dao.snippets().storeSnippets({ + storeSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("apiId"), + sdk: { + type: "python", + sdk: { + package: "acme", + version: "0.0.1", }, - }); - // get snippets - const response = await fdrApplication.dao.snippets().loadSnippetsPage({ - loadSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("apiId"), - endpointIdentifier: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + async_client: "invalid", + sync_client: "invalid", }, - sdks: undefined, - page: undefined, exampleIdentifier: "example1", - }, - }); - expect(response).not.toEqual(undefined); - expect(Object.keys(response?.snippets ?? {}).length).toEqual(1); + }, + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + async_client: "client = AsyncAcme(api_key='YOUR_API_KEY')", + sync_client: "client = Acme(api_key='YOUR_API_KEY')", + }, + exampleIdentifier: "example2", + }, + ], + }, + }, + }); + // get snippets + const response = await fdrApplication.dao.snippets().loadSnippetsPage({ + loadSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("apiId"), + endpointIdentifier: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + sdks: undefined, + page: undefined, + exampleIdentifier: "example1", + }, + }); + expect(response).not.toEqual(undefined); + expect(Object.keys(response?.snippets ?? {}).length).toEqual(1); - const snippets = response?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; - if (snippets === undefined) { - throw new Error("snippets were undefined"); - } - expect(snippets).not.toEqual(undefined); - expect(snippets.length).toEqual(1); + const snippets = + response?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; + if (snippets === undefined) { + throw new Error("snippets were undefined"); + } + expect(snippets).not.toEqual(undefined); + expect(snippets.length).toEqual(1); - const snippet = snippets[0]; - if (snippet === undefined) { - throw new Error("snippet was undefined"); - } - expect(snippet).not.toEqual(undefined); - expect(snippet.type).toEqual("python"); + const snippet = snippets[0]; + if (snippet === undefined) { + throw new Error("snippet was undefined"); + } + expect(snippet).not.toEqual(undefined); + expect(snippet.type).toEqual("python"); - if (snippet.type != "python") { - throw new Error("expected a python snippet"); - } - expect(snippet.sdk.package).toEqual("acme"); - expect(snippet.sdk.version).toEqual("0.0.1"); - expect(snippet.exampleIdentifier).toEqual("example1"); - expect(snippet.async_client).toEqual("invalid"); - expect(snippet.sync_client).toEqual("invalid"); + if (snippet.type != "python") { + throw new Error("expected a python snippet"); + } + expect(snippet.sdk.package).toEqual("acme"); + expect(snippet.sdk.version).toEqual("0.0.1"); + expect(snippet.exampleIdentifier).toEqual("example1"); + expect(snippet.async_client).toEqual("invalid"); + expect(snippet.sync_client).toEqual("invalid"); - const response2 = await fdrApplication.dao.snippets().loadSnippetsPage({ - loadSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("apiId"), - endpointIdentifier: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - sdks: undefined, - page: undefined, - exampleIdentifier: "example2", - }, - }); - expect(response2).not.toEqual(undefined); - expect(Object.keys(response2?.snippets ?? {}).length).toEqual(1); + const response2 = await fdrApplication.dao.snippets().loadSnippetsPage({ + loadSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("apiId"), + endpointIdentifier: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + sdks: undefined, + page: undefined, + exampleIdentifier: "example2", + }, + }); + expect(response2).not.toEqual(undefined); + expect(Object.keys(response2?.snippets ?? {}).length).toEqual(1); - const snippets2 = response2?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; - if (snippets2 === undefined) { - throw new Error("snippets2 were undefined"); - } - expect(snippets2).not.toEqual(undefined); - expect(snippets2.length).toEqual(1); + const snippets2 = + response2?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; + if (snippets2 === undefined) { + throw new Error("snippets2 were undefined"); + } + expect(snippets2).not.toEqual(undefined); + expect(snippets2.length).toEqual(1); - const snippet2 = snippets2[0]; - if (snippet2 === undefined) { - throw new Error("snippet2 was undefined"); - } - expect(snippet2).not.toEqual(undefined); - expect(snippet2.type).toEqual("python"); + const snippet2 = snippets2[0]; + if (snippet2 === undefined) { + throw new Error("snippet2 was undefined"); + } + expect(snippet2).not.toEqual(undefined); + expect(snippet2.type).toEqual("python"); - if (snippet2.type != "python") { - throw new Error("expected a python snippet2"); - } - expect(snippet2.sdk.package).toEqual("acme"); - expect(snippet2.sdk.version).toEqual("0.0.1"); - expect(snippet2.exampleIdentifier).toEqual("example2"); - expect(snippet2.async_client).toEqual("client = AsyncAcme(api_key='YOUR_API_KEY')"); - expect(snippet2.sync_client).toEqual("client = Acme(api_key='YOUR_API_KEY')"); + if (snippet2.type != "python") { + throw new Error("expected a python snippet2"); + } + expect(snippet2.sdk.package).toEqual("acme"); + expect(snippet2.sdk.version).toEqual("0.0.1"); + expect(snippet2.exampleIdentifier).toEqual("example2"); + expect(snippet2.async_client).toEqual( + "client = AsyncAcme(api_key='YOUR_API_KEY')" + ); + expect(snippet2.sync_client).toEqual("client = Acme(api_key='YOUR_API_KEY')"); - const response3 = await fdrApplication.dao.snippets().loadSnippetsPage({ - loadSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("apiId"), - endpointIdentifier: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - sdks: undefined, - page: undefined, - exampleIdentifier: undefined, - }, - }); - const snippets3 = response3?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; - if (snippets3 === undefined) { - throw new Error("snippets3 were undefined"); - } - expect(snippets3).not.toEqual(undefined); - expect(snippets3.length).toEqual(2); + const response3 = await fdrApplication.dao.snippets().loadSnippetsPage({ + loadSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("apiId"), + endpointIdentifier: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + sdks: undefined, + page: undefined, + exampleIdentifier: undefined, + }, + }); + const snippets3 = + response3?.snippets[FdrAPI.EndpointPathLiteral("/users/v1")]?.GET; + if (snippets3 === undefined) { + throw new Error("snippets3 were undefined"); + } + expect(snippets3).not.toEqual(undefined); + expect(snippets3.length).toEqual(2); }); it("snippets template", async () => { - await fdrApplication.dao.snippetTemplates().storeSnippetTemplate({ - storeSnippetsInfo: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - apiDefinitionId: FdrAPI.ApiDefinitionId("...."), - snippets: [ - { - sdk: { - type: "python", - package: "acme", - version: "0.0.1", - }, - endpointId: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippetTemplate: { - type: "v1", - clientInstantiation: "", - functionInvocation: { - type: "generic", - isOptional: false, - templateString: "", - imports: undefined, - templateInputs: undefined, - inputDelimiter: undefined, - }, - }, - additionalTemplates: undefined, - }, - { - sdk: { - type: "typescript", - package: "acme", - version: "0.0.1", - }, - endpointId: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippetTemplate: { - type: "v1", - clientInstantiation: "", - functionInvocation: { - type: "generic", - isOptional: false, - templateString: "", - imports: undefined, - templateInputs: undefined, - inputDelimiter: undefined, - }, - }, - additionalTemplates: undefined, - }, - ], - }, - }); - - const response = await fdrApplication.dao.snippetTemplates().loadSnippetTemplate({ - loadSnippetTemplateRequest: { - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - endpointId: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - sdk: { - type: "python", - package: "acme", - version: "0.0.1", + await fdrApplication.dao.snippetTemplates().storeSnippetTemplate({ + storeSnippetsInfo: { + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + apiDefinitionId: FdrAPI.ApiDefinitionId("...."), + snippets: [ + { + sdk: { + type: "python", + package: "acme", + version: "0.0.1", + }, + endpointId: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippetTemplate: { + type: "v1", + clientInstantiation: "", + functionInvocation: { + type: "generic", + isOptional: false, + templateString: "", + imports: undefined, + templateInputs: undefined, + inputDelimiter: undefined, }, + }, + additionalTemplates: undefined, }, - }); - - expect(response).not.toEqual(null); - expect(response).toEqual({ - additionalTemplates: undefined, - apiDefinitionId: FdrAPI.ApiDefinitionId("...."), - endpointId: { identifierOverride: undefined, path: FdrAPI.EndpointPathLiteral("/users/v1"), method: "GET" }, - sdk: { type: "python", package: "acme", version: "0.0.1" }, - snippetTemplate: { + { + sdk: { + type: "typescript", + package: "acme", + version: "0.0.1", + }, + endpointId: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippetTemplate: { type: "v1", - functionInvocation: { type: "generic", isOptional: false, templateString: "" }, clientInstantiation: "", + functionInvocation: { + type: "generic", + isOptional: false, + templateString: "", + imports: undefined, + templateInputs: undefined, + inputDelimiter: undefined, + }, + }, + additionalTemplates: undefined, }, - }); + ], + }, + }); - const response2 = await fdrApplication.dao.snippetTemplates().loadSnippetTemplatesByEndpoint({ + const response = await fdrApplication.dao + .snippetTemplates() + .loadSnippetTemplate({ + loadSnippetTemplateRequest: { orgId: FdrAPI.OrgId("acme"), apiId: FdrAPI.ApiId("api"), - sdkRequests: [ - { - type: "python", - package: "acme", - version: undefined, - }, - { - type: "typescript", - package: "acme", - version: undefined, - }, - ], - definition: { - rootPackage: { - endpoints: [ - { - id: FdrAPI.EndpointId("getUsers"), - path: { - parts: [{ type: "literal", value: "/users/v1" }], - pathParameters: [], - }, - method: "GET", - queryParameters: [], - headers: [], - examples: [], - auth: undefined, - defaultEnvironment: undefined, - environments: undefined, - originalEndpointId: undefined, - name: undefined, - request: undefined, - response: undefined, - errors: undefined, - errorsV2: undefined, - description: undefined, - availability: undefined, - }, - ], - types: [], - subpackages: [], - websockets: undefined, - webhooks: undefined, - pointsTo: undefined, - }, - types: {}, - subpackages: {}, - auth: undefined, - globalHeaders: undefined, - snippetsConfiguration: undefined, - navigation: undefined, + endpointId: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, }, + sdk: { + type: "python", + package: "acme", + version: "0.0.1", + }, + }, }); - expect(response2).toEqual({ - "/users/v1": { - PATCH: {}, - POST: {}, - PUT: {}, - GET: { - python: { - type: "v1", - functionInvocation: { - type: "generic", - isOptional: false, - templateString: "", - }, - clientInstantiation: "", - }, - typescript: { - type: "v1", - functionInvocation: { - type: "generic", - isOptional: false, - templateString: "", - }, - clientInstantiation: "", - }, + expect(response).not.toEqual(null); + expect(response).toEqual({ + additionalTemplates: undefined, + apiDefinitionId: FdrAPI.ApiDefinitionId("...."), + endpointId: { + identifierOverride: undefined, + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: "GET", + }, + sdk: { type: "python", package: "acme", version: "0.0.1" }, + snippetTemplate: { + type: "v1", + functionInvocation: { + type: "generic", + isOptional: false, + templateString: "", + }, + clientInstantiation: "", + }, + }); + + const response2 = await fdrApplication.dao + .snippetTemplates() + .loadSnippetTemplatesByEndpoint({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + sdkRequests: [ + { + type: "python", + package: "acme", + version: undefined, + }, + { + type: "typescript", + package: "acme", + version: undefined, + }, + ], + definition: { + rootPackage: { + endpoints: [ + { + id: FdrAPI.EndpointId("getUsers"), + path: { + parts: [{ type: "literal", value: "/users/v1" }], + pathParameters: [], + }, + method: "GET", + queryParameters: [], + headers: [], + examples: [], + auth: undefined, + defaultEnvironment: undefined, + environments: undefined, + originalEndpointId: undefined, + name: undefined, + request: undefined, + response: undefined, + errors: undefined, + errorsV2: undefined, + description: undefined, + availability: undefined, }, - DELETE: {}, + ], + types: [], + subpackages: [], + websockets: undefined, + webhooks: undefined, + pointsTo: undefined, }, + types: {}, + subpackages: {}, + auth: undefined, + globalHeaders: undefined, + snippetsConfiguration: undefined, + navigation: undefined, + }, }); - const response3 = await fdrApplication.dao.snippetTemplates().loadSnippetTemplatesByEndpoint({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - sdkRequests: [ + expect(response2).toEqual({ + "/users/v1": { + PATCH: {}, + POST: {}, + PUT: {}, + GET: { + python: { + type: "v1", + functionInvocation: { + type: "generic", + isOptional: false, + templateString: "", + }, + clientInstantiation: "", + }, + typescript: { + type: "v1", + functionInvocation: { + type: "generic", + isOptional: false, + templateString: "", + }, + clientInstantiation: "", + }, + }, + DELETE: {}, + }, + }); + + const response3 = await fdrApplication.dao + .snippetTemplates() + .loadSnippetTemplatesByEndpoint({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + sdkRequests: [ + { + type: "go", + githubRepo: "", + version: undefined, + }, + ], + definition: { + rootPackage: { + endpoints: [ { - type: "go", - githubRepo: "", - version: undefined, - }, - ], - definition: { - rootPackage: { - endpoints: [ - { - id: FdrAPI.EndpointId("getUsers"), - path: { - parts: [{ type: "literal", value: "/users/v1" }], - pathParameters: [], - }, - method: "GET", - queryParameters: [], - headers: [], - examples: [], - auth: undefined, - defaultEnvironment: undefined, - environments: undefined, - originalEndpointId: undefined, - name: undefined, - request: undefined, - response: undefined, - errors: undefined, - errorsV2: undefined, - description: undefined, - availability: undefined, - }, - ], - types: [], - subpackages: [], - websockets: undefined, - webhooks: undefined, - pointsTo: undefined, + id: FdrAPI.EndpointId("getUsers"), + path: { + parts: [{ type: "literal", value: "/users/v1" }], + pathParameters: [], + }, + method: "GET", + queryParameters: [], + headers: [], + examples: [], + auth: undefined, + defaultEnvironment: undefined, + environments: undefined, + originalEndpointId: undefined, + name: undefined, + request: undefined, + response: undefined, + errors: undefined, + errorsV2: undefined, + description: undefined, + availability: undefined, }, - types: {}, - subpackages: {}, - auth: undefined, - globalHeaders: undefined, - snippetsConfiguration: undefined, - navigation: undefined, + ], + types: [], + subpackages: [], + websockets: undefined, + webhooks: undefined, + pointsTo: undefined, }, + types: {}, + subpackages: {}, + auth: undefined, + globalHeaders: undefined, + snippetsConfiguration: undefined, + navigation: undefined, + }, }); - expect(response3).toEqual({}); + expect(response3).toEqual({}); }); diff --git a/servers/fdr/src/__test__/local/revalidate.test.ts b/servers/fdr/src/__test__/local/revalidate.test.ts index 48b21cc8dc..00a5ef58de 100644 --- a/servers/fdr/src/__test__/local/revalidate.test.ts +++ b/servers/fdr/src/__test__/local/revalidate.test.ts @@ -2,15 +2,15 @@ import { RevalidatorServiceImpl } from "../../services/revalidator/RevalidatorSe import { ParsedBaseUrl } from "../../util/ParsedBaseUrl"; it.skip("revalidates a custom docs domain", async () => { - const revalidationService = new RevalidatorServiceImpl(); + const revalidationService = new RevalidatorServiceImpl(); - const revalidationResult = await revalidationService.revalidate({ - baseUrl: ParsedBaseUrl.parse("https://fdr-ete-test.buildwithfern.com"), - }); + const revalidationResult = await revalidationService.revalidate({ + baseUrl: ParsedBaseUrl.parse("https://fdr-ete-test.buildwithfern.com"), + }); - expect(revalidationResult.revalidationFailed).toEqual(false); + expect(revalidationResult.revalidationFailed).toEqual(false); - expect(revalidationResult.failed.length).toEqual(0); + expect(revalidationResult.failed.length).toEqual(0); - expect(revalidationResult.successful.length).toBeGreaterThan(0); + expect(revalidationResult.successful.length).toBeGreaterThan(0); }); diff --git a/servers/fdr/src/__test__/local/s3.test.ts b/servers/fdr/src/__test__/local/s3.test.ts index efcc9ce5c6..70d985e486 100644 --- a/servers/fdr/src/__test__/local/s3.test.ts +++ b/servers/fdr/src/__test__/local/s3.test.ts @@ -4,67 +4,69 @@ import { v4 as uuidv4 } from "uuid"; import { S3ServiceImpl } from "../../services/s3"; describe("S3 Service", () => { - it.skip("Test S3 Upload URLs", async () => { - const s3Service = new S3ServiceImpl({ - venusUrl: "string", - awsAccessKey: process.env.AWS_ACCESS_KEY_ID ?? "", - awsSecretKey: process.env.AWS_SECRET_ACCESS_KEY ?? "", - publicDocsS3: { - bucketName: "fdr-dev2-docs-files-public", - bucketRegion: "us-east-1", - urlOverride: undefined, - }, - privateDocsS3: { - bucketName: "fdr-dev2-docs-files", - bucketRegion: "us-east-1", - urlOverride: undefined, - }, - privateApiDefinitionSourceS3: { - bucketName: "fdr-source-files", - bucketRegion: "us-east-1", - urlOverride: undefined, - }, - domainSuffix: "string", - algoliaAppId: "string", - algoliaAdminApiKey: "string", - algoliaSearchApiKey: "string", - algoliaSearchIndex: "string", - algoliaSearchV2Domains: ["string"], - slackToken: "string", - logLevel: "string", - docsCacheEndpoint: "string", - enableCustomerNotifications: true, - redisEnabled: true, - redisClusteringEnabled: true, - applicationEnvironment: "string", - cdnPublicDocsUrl: "string", - }); - const startUploadDocsResponse = await s3Service.createPresignedDocsAssetsUploadUrlWithClient({ - domain: "buildwithfern.com", - time: "12340", - isPrivate: false, - filepath: DocsV1Write.FilePath("deep"), - }); - console.log(startUploadDocsResponse.url); - expect(true).toEqual(true); - const uploadDocsResponse = await fetch(startUploadDocsResponse.url, { - method: "PUT", - body: await readFile(""), - }); - expect(uploadDocsResponse.status).toEqual(200); + it.skip("Test S3 Upload URLs", async () => { + const s3Service = new S3ServiceImpl({ + venusUrl: "string", + awsAccessKey: process.env.AWS_ACCESS_KEY_ID ?? "", + awsSecretKey: process.env.AWS_SECRET_ACCESS_KEY ?? "", + publicDocsS3: { + bucketName: "fdr-dev2-docs-files-public", + bucketRegion: "us-east-1", + urlOverride: undefined, + }, + privateDocsS3: { + bucketName: "fdr-dev2-docs-files", + bucketRegion: "us-east-1", + urlOverride: undefined, + }, + privateApiDefinitionSourceS3: { + bucketName: "fdr-source-files", + bucketRegion: "us-east-1", + urlOverride: undefined, + }, + domainSuffix: "string", + algoliaAppId: "string", + algoliaAdminApiKey: "string", + algoliaSearchApiKey: "string", + algoliaSearchIndex: "string", + algoliaSearchV2Domains: ["string"], + slackToken: "string", + logLevel: "string", + docsCacheEndpoint: "string", + enableCustomerNotifications: true, + redisEnabled: true, + redisClusteringEnabled: true, + applicationEnvironment: "string", + cdnPublicDocsUrl: "string", + }); + const startUploadDocsResponse = + await s3Service.createPresignedDocsAssetsUploadUrlWithClient({ + domain: "buildwithfern.com", + time: "12340", + isPrivate: false, + filepath: DocsV1Write.FilePath("deep"), + }); + console.log(startUploadDocsResponse.url); + expect(true).toEqual(true); + const uploadDocsResponse = await fetch(startUploadDocsResponse.url, { + method: "PUT", + body: await readFile(""), + }); + expect(uploadDocsResponse.status).toEqual(200); - const startUploadSourceResponse = await s3Service.createPresignedApiDefinitionSourceUploadUrlWithClient({ - orgId: FdrAPI.OrgId("fern-api"), - apiId: FdrAPI.ApiId("fern"), - time: "12340", - sourceId: APIV1Write.SourceId(uuidv4()), - }); - console.log(startUploadSourceResponse.url); - expect(true).toEqual(true); - const uploadSourceResponse = await fetch(startUploadSourceResponse.url, { - method: "PUT", - body: await readFile(""), - }); - expect(uploadSourceResponse.status).toEqual(200); + const startUploadSourceResponse = + await s3Service.createPresignedApiDefinitionSourceUploadUrlWithClient({ + orgId: FdrAPI.OrgId("fern-api"), + apiId: FdrAPI.ApiId("fern"), + time: "12340", + sourceId: APIV1Write.SourceId(uuidv4()), + }); + console.log(startUploadSourceResponse.url); + expect(true).toEqual(true); + const uploadSourceResponse = await fetch(startUploadSourceResponse.url, { + method: "PUT", + body: await readFile(""), }); + expect(uploadSourceResponse.status).toEqual(200); + }); }); diff --git a/servers/fdr/src/__test__/local/services/api.test.ts b/servers/fdr/src/__test__/local/services/api.test.ts index 853736b7f0..d2087d812e 100644 --- a/servers/fdr/src/__test__/local/services/api.test.ts +++ b/servers/fdr/src/__test__/local/services/api.test.ts @@ -3,72 +3,83 @@ import { inject } from "vitest"; import { createApiDefinition, getAPIResponse, getClient } from "../util"; export const EMPTY_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = { - rootPackage: { - endpoints: [], - webhooks: [], - websockets: [], - types: [], - subpackages: [], - pointsTo: undefined, - }, - subpackages: {}, - types: {}, - auth: undefined, - globalHeaders: undefined, - snippetsConfiguration: undefined, - navigation: undefined, + rootPackage: { + endpoints: [], + webhooks: [], + websockets: [], + types: [], + subpackages: [], + pointsTo: undefined, + }, + subpackages: {}, + types: {}, + auth: undefined, + globalHeaders: undefined, + snippetsConfiguration: undefined, + navigation: undefined, }; -const MOCK_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = createApiDefinition({ +const MOCK_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = + createApiDefinition({ endpointId: APIV1Write.EndpointId("dummy"), endpointMethod: "POST", endpointPath: { - parts: [{ type: "literal", value: "dummy" }], - pathParameters: [], + parts: [{ type: "literal", value: "dummy" }], + pathParameters: [], }, -}); + }); it("register api", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register empty definition - const emptyDefinitionRegisterResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("fern"), - apiId: FdrAPI.ApiId("api"), - definition: EMPTY_REGISTER_API_DEFINITION, - }), - ); + const fdr = getClient({ authed: true, url: inject("url") }); + // register empty definition + const emptyDefinitionRegisterResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("fern"), + apiId: FdrAPI.ApiId("api"), + definition: EMPTY_REGISTER_API_DEFINITION, + }) + ); - console.log(`Registered empty definition. Received ${emptyDefinitionRegisterResponse.apiDefinitionId}`); - // load empty definition - const registeredEmptyDefinition = getAPIResponse( - await fdr.api.v1.read.getApi(emptyDefinitionRegisterResponse.apiDefinitionId), - ); + console.log( + `Registered empty definition. Received ${emptyDefinitionRegisterResponse.apiDefinitionId}` + ); + // load empty definition + const registeredEmptyDefinition = getAPIResponse( + await fdr.api.v1.read.getApi( + emptyDefinitionRegisterResponse.apiDefinitionId + ) + ); - // assert definitions are equal - expect(JSON.stringify(registeredEmptyDefinition.types)).toEqual( - JSON.stringify(EMPTY_REGISTER_API_DEFINITION.types), - ); - expect(JSON.stringify(registeredEmptyDefinition.subpackages)).toEqual( - JSON.stringify(EMPTY_REGISTER_API_DEFINITION.subpackages), - ); - expect(registeredEmptyDefinition.rootPackage).toEqual(EMPTY_REGISTER_API_DEFINITION.rootPackage); + // assert definitions are equal + expect(JSON.stringify(registeredEmptyDefinition.types)).toEqual( + JSON.stringify(EMPTY_REGISTER_API_DEFINITION.types) + ); + expect(JSON.stringify(registeredEmptyDefinition.subpackages)).toEqual( + JSON.stringify(EMPTY_REGISTER_API_DEFINITION.subpackages) + ); + expect(registeredEmptyDefinition.rootPackage).toEqual( + EMPTY_REGISTER_API_DEFINITION.rootPackage + ); - // register updated definition - const updatedDefinitionRegisterResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("fern"), - apiId: FdrAPI.ApiId("api"), - definition: MOCK_REGISTER_API_DEFINITION, - }), - ); - // load updated definition - const updatedDefinition = getAPIResponse( - await fdr.api.v1.read.getApi(updatedDefinitionRegisterResponse.apiDefinitionId), - ); - // assert definitions equal - expect(JSON.stringify(updatedDefinition.types)).toEqual(JSON.stringify(MOCK_REGISTER_API_DEFINITION.types)); - expect(JSON.stringify(updatedDefinition.subpackages)).toEqual( - JSON.stringify(MOCK_REGISTER_API_DEFINITION.subpackages), - ); + // register updated definition + const updatedDefinitionRegisterResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("fern"), + apiId: FdrAPI.ApiId("api"), + definition: MOCK_REGISTER_API_DEFINITION, + }) + ); + // load updated definition + const updatedDefinition = getAPIResponse( + await fdr.api.v1.read.getApi( + updatedDefinitionRegisterResponse.apiDefinitionId + ) + ); + // assert definitions equal + expect(JSON.stringify(updatedDefinition.types)).toEqual( + JSON.stringify(MOCK_REGISTER_API_DEFINITION.types) + ); + expect(JSON.stringify(updatedDefinition.subpackages)).toEqual( + JSON.stringify(MOCK_REGISTER_API_DEFINITION.subpackages) + ); }); diff --git a/servers/fdr/src/__test__/local/services/diff.test.ts b/servers/fdr/src/__test__/local/services/diff.test.ts index d3a3350fe2..9d7a12b5ef 100644 --- a/servers/fdr/src/__test__/local/services/diff.test.ts +++ b/servers/fdr/src/__test__/local/services/diff.test.ts @@ -3,57 +3,58 @@ import { inject } from "vitest"; import { createApiDefinition, getAPIResponse, getClient } from "../util"; export const EMPTY_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = { - rootPackage: { - endpoints: [], - webhooks: [], - websockets: [], - types: [], - subpackages: [], - pointsTo: undefined, - }, - subpackages: {}, - types: {}, - auth: undefined, - globalHeaders: undefined, - snippetsConfiguration: undefined, - navigation: undefined, + rootPackage: { + endpoints: [], + webhooks: [], + websockets: [], + types: [], + subpackages: [], + pointsTo: undefined, + }, + subpackages: {}, + types: {}, + auth: undefined, + globalHeaders: undefined, + snippetsConfiguration: undefined, + navigation: undefined, }; -const MOCK_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = createApiDefinition({ +const MOCK_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = + createApiDefinition({ endpointId: APIV1Write.EndpointId("dummy"), endpointMethod: "POST", endpointPath: { - parts: [{ type: "literal", value: "dummy" }], - pathParameters: [], + parts: [{ type: "literal", value: "dummy" }], + pathParameters: [], }, -}); + }); it("register api", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register empty definition - const emptyDefinitionRegisterResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("fern"), - apiId: FdrAPI.ApiId("api"), - definition: EMPTY_REGISTER_API_DEFINITION, - }), - ); + const fdr = getClient({ authed: true, url: inject("url") }); + // register empty definition + const emptyDefinitionRegisterResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("fern"), + apiId: FdrAPI.ApiId("api"), + definition: EMPTY_REGISTER_API_DEFINITION, + }) + ); - // register updated definition - const updatedDefinitionRegisterResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("fern"), - apiId: FdrAPI.ApiId("api"), - definition: MOCK_REGISTER_API_DEFINITION, - }), - ); + // register updated definition + const updatedDefinitionRegisterResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("fern"), + apiId: FdrAPI.ApiId("api"), + definition: MOCK_REGISTER_API_DEFINITION, + }) + ); - const response = getAPIResponse( - await fdr.diff.diff({ - currentApiDefinitionId: updatedDefinitionRegisterResponse.apiDefinitionId, - previousApiDefinitionId: emptyDefinitionRegisterResponse.apiDefinitionId, - }), - ); + const response = getAPIResponse( + await fdr.diff.diff({ + currentApiDefinitionId: updatedDefinitionRegisterResponse.apiDefinitionId, + previousApiDefinitionId: emptyDefinitionRegisterResponse.apiDefinitionId, + }) + ); - expect(response.markdown.length).toBeGreaterThan(0); + expect(response.markdown.length).toBeGreaterThan(0); }); diff --git a/servers/fdr/src/__test__/local/services/docs.test.ts b/servers/fdr/src/__test__/local/services/docs.test.ts index 9632d11fad..268cffb1ce 100644 --- a/servers/fdr/src/__test__/local/services/docs.test.ts +++ b/servers/fdr/src/__test__/local/services/docs.test.ts @@ -5,245 +5,270 @@ import { getAPIResponse, getClient } from "../util"; export const FONT_FILE_ID = DocsV1Write.FileId(uniqueId()); export const WRITE_DOCS_REGISTER_DEFINITION: DocsV1Write.DocsDefinition = { - pages: {}, - config: { - navigation: { - items: [], - landingPage: undefined, - }, - root: undefined, - typography: { - headingsFont: { - name: "Syne", - fontFile: FONT_FILE_ID, - }, - bodyFont: undefined, - codeFont: undefined, - }, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - backgroundImage: undefined, - logoV2: undefined, - logo: undefined, - colors: undefined, - colorsV2: undefined, + pages: {}, + config: { + navigation: { + items: [], + landingPage: undefined, }, - jsFiles: undefined, + root: undefined, + typography: { + headingsFont: { + name: "Syne", + fontFile: FONT_FILE_ID, + }, + bodyFont: undefined, + codeFont: undefined, + }, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + backgroundImage: undefined, + logoV2: undefined, + logo: undefined, + colors: undefined, + colorsV2: undefined, + }, + jsFiles: undefined, }; it("docs register", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - const domain = `docs-${Math.random()}.fern.com`; - - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v1.write.startDocsRegister({ - orgId: FdrAPI.OrgId("fern"), - domain, - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - }), - ); - await fdr.docs.v1.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); - - // load docs - const docs = getAPIResponse( - await fdr.docs.v1.read.getDocsForDomain({ - domain, - }), - ); - // assert docs have 2 file urls - expect(Object.entries(docs.files)).toHaveLength(2); - - // re-register docs - const startDocsRegisterResponse2 = getAPIResponse( - await fdr.docs.v1.write.startDocsRegister({ - orgId: FdrAPI.OrgId("fern"), - domain, - filepaths: [], - }), - ); - await fdr.docs.v1.write.finishDocsRegister(startDocsRegisterResponse2.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); + const fdr = getClient({ authed: true, url: inject("url") }); + const domain = `docs-${Math.random()}.fern.com`; + + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v1.write.startDocsRegister({ + orgId: FdrAPI.OrgId("fern"), + domain, + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + }) + ); + await fdr.docs.v1.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, + } + ); + + // load docs + const docs = getAPIResponse( + await fdr.docs.v1.read.getDocsForDomain({ + domain, + }) + ); + // assert docs have 2 file urls + expect(Object.entries(docs.files)).toHaveLength(2); + + // re-register docs + const startDocsRegisterResponse2 = getAPIResponse( + await fdr.docs.v1.write.startDocsRegister({ + orgId: FdrAPI.OrgId("fern"), + domain, + filepaths: [], + }) + ); + await fdr.docs.v1.write.finishDocsRegister( + startDocsRegisterResponse2.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, + } + ); }); it("test invalid domain URL - special characters", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - const domain = `https://fern-${Math.random()}.docs.buildwithfern.com`; - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId(`plantstore-2024-test${Math.random()}`), - apiId: FdrAPI.ApiId(""), - domain, - customDomains: [], - filepaths: [ - DocsV1Write.FilePath("logo.png"), - DocsV1Write.FilePath("guides/guide.mdx"), - DocsV1Write.FilePath("fonts/Syne.woff2"), - ], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); - - const startDocsRegisterResponse2 = await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId(`plantstore-2024-test${Math.random()}`), - apiId: FdrAPI.ApiId(""), - domain: domain + "//", - customDomains: [], - filepaths: [ - DocsV1Write.FilePath("logo.png"), - DocsV1Write.FilePath("guides/guide.mdx"), - DocsV1Write.FilePath("fonts/Syne.woff2"), - ], - }); - - // expecting an error, because adding // to the domain should not bypass domain check - expect((startDocsRegisterResponse2 as any).error.content).toEqual({ - body: `Domain URL is malformed: ${domain + "//"}`, - reason: "status-code", - statusCode: 400, - }); + const fdr = getClient({ authed: true, url: inject("url") }); + const domain = `https://fern-${Math.random()}.docs.buildwithfern.com`; + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId(`plantstore-2024-test${Math.random()}`), + apiId: FdrAPI.ApiId(""), + domain, + customDomains: [], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + DocsV1Write.FilePath("fonts/Syne.woff2"), + ], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, + } + ); + + const startDocsRegisterResponse2 = await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId(`plantstore-2024-test${Math.random()}`), + apiId: FdrAPI.ApiId(""), + domain: domain + "//", + customDomains: [], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + DocsV1Write.FilePath("fonts/Syne.woff2"), + ], + }); + + // expecting an error, because adding // to the domain should not bypass domain check + expect((startDocsRegisterResponse2 as any).error.content).toEqual({ + body: `Domain URL is malformed: ${domain + "//"}`, + reason: "status-code", + statusCode: 400, + }); }); it("docs register V2", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: ["https://docs.useacme.com/docs"], - filepaths: [ - DocsV1Write.FilePath("logo.png"), - DocsV1Write.FilePath("guides/guide.mdx"), - DocsV1Write.FilePath("fonts/Syne.woff2"), - ], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); - // load docs - let docs = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: DocsV1Write.Url("https://acme.docs.buildwithfern.com/my/random/slug"), - }), - ); - expect(docs.baseUrl.domain).toEqual("acme.docs.buildwithfern.com"); - expect(Object.entries(docs.definition.files)).toHaveLength(3); - expect(docs.definition.config.typographyV2).toEqual({ - headingsFont: { - type: "custom", - name: "Syne", - variants: [{ fontFile: FONT_FILE_ID }], - }, - }); - // load docs again - docs = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: DocsV1Write.Url("https://docs.useacme.com/docs/1/"), - }), - ); - expect(docs.baseUrl.domain).toEqual("docs.useacme.com"); - expect(docs.baseUrl.basePath).toEqual("/docs"); - expect(Object.entries(docs.definition.files)).toHaveLength(3); - - //re-register docs - const startDocsRegisterResponse2 = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: ["https://docs.useacme.com"], - filepaths: [], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse2.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); + const fdr = getClient({ authed: true, url: inject("url") }); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: ["https://docs.useacme.com/docs"], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + DocsV1Write.FilePath("fonts/Syne.woff2"), + ], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, + } + ); + // load docs + let docs = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: DocsV1Write.Url( + "https://acme.docs.buildwithfern.com/my/random/slug" + ), + }) + ); + expect(docs.baseUrl.domain).toEqual("acme.docs.buildwithfern.com"); + expect(Object.entries(docs.definition.files)).toHaveLength(3); + expect(docs.definition.config.typographyV2).toEqual({ + headingsFont: { + type: "custom", + name: "Syne", + variants: [{ fontFile: FONT_FILE_ID }], + }, + }); + // load docs again + docs = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: DocsV1Write.Url("https://docs.useacme.com/docs/1/"), + }) + ); + expect(docs.baseUrl.domain).toEqual("docs.useacme.com"); + expect(docs.baseUrl.basePath).toEqual("/docs"); + expect(Object.entries(docs.definition.files)).toHaveLength(3); + + //re-register docs + const startDocsRegisterResponse2 = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: ["https://docs.useacme.com"], + filepaths: [], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse2.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, + } + ); }); it("docs reindex", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("api"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: ["https://docs.useacme.com/docs"], - filepaths: [ - DocsV1Write.FilePath("logo.png"), - DocsV1Write.FilePath("guides/guide.mdx"), - DocsV1Write.FilePath("fonts/Syne.woff2"), - ], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); - - const first = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: DocsV1Write.Url("https://acme.docs.buildwithfern.com"), - }), - ); - - const response = await fdr.docs.v2.write.reindexAlgoliaSearchRecords({ - url: DocsV1Write.Url("https://acme.docs.buildwithfern.com"), - }); - - expect(response.ok).toBeTruthy(); - - const second = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: DocsV1Write.Url("https://acme.docs.buildwithfern.com"), - }), - ); - - if ( - first.definition.search.type === "legacyMultiAlgoliaIndex" || - second.definition.search.type === "legacyMultiAlgoliaIndex" - ) { - throw new Error("Expected search type to be 'singleAlgoliaIndex'"); + const fdr = getClient({ authed: true, url: inject("url") }); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("api"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: ["https://docs.useacme.com/docs"], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + DocsV1Write.FilePath("fonts/Syne.woff2"), + ], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, } + ); + + const first = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: DocsV1Write.Url("https://acme.docs.buildwithfern.com"), + }) + ); + + const response = await fdr.docs.v2.write.reindexAlgoliaSearchRecords({ + url: DocsV1Write.Url("https://acme.docs.buildwithfern.com"), + }); - expect(first.definition.search.value).not.toEqual(second.definition.search.value); + expect(response.ok).toBeTruthy(); + + const second = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: DocsV1Write.Url("https://acme.docs.buildwithfern.com"), + }) + ); + + if ( + first.definition.search.type === "legacyMultiAlgoliaIndex" || + second.definition.search.type === "legacyMultiAlgoliaIndex" + ) { + throw new Error("Expected search type to be 'singleAlgoliaIndex'"); + } + + expect(first.definition.search.value).not.toEqual( + second.definition.search.value + ); }); test.sequential("revalidates a custom docs domain", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); + const fdr = getClient({ authed: true, url: inject("url") }); - const resp = await fdr.docs.v2.read.listAllDocsUrls(); - console.log(resp); + const resp = await fdr.docs.v2.read.listAllDocsUrls(); + console.log(resp); - if (!resp.ok) { - throw new Error("Failed to list all docs urls"); - } + if (!resp.ok) { + throw new Error("Failed to list all docs urls"); + } - const { urls } = resp.body; - console.log(urls); + const { urls } = resp.body; + console.log(urls); - expect(urls.length).toBeGreaterThan(0); + expect(urls.length).toBeGreaterThan(0); }); diff --git a/servers/fdr/src/__test__/local/services/docsCache.test.ts b/servers/fdr/src/__test__/local/services/docsCache.test.ts index 571f623e6d..d6eda896de 100644 --- a/servers/fdr/src/__test__/local/services/docsCache.test.ts +++ b/servers/fdr/src/__test__/local/services/docsCache.test.ts @@ -3,59 +3,65 @@ import { inject } from "vitest"; import { getAPIResponse, getClient } from "../util"; export const WRITE_DOCS_REGISTER_DEFINITION: DocsV1Write.DocsDefinition = { - pages: {}, - config: { - navigation: { - items: [], - landingPage: undefined, - }, - root: undefined, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - backgroundImage: undefined, - logoV2: undefined, - logo: undefined, - colors: undefined, - colorsV2: undefined, - typography: undefined, + pages: {}, + config: { + navigation: { + items: [], + landingPage: undefined, }, - jsFiles: undefined, + root: undefined, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + backgroundImage: undefined, + logoV2: undefined, + logo: undefined, + colors: undefined, + colorsV2: undefined, + typography: undefined, + }, + jsFiles: undefined, }; it("docs invalidate cache", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - const domain = `docs-${Math.random()}.fern.com`; + const fdr = getClient({ authed: true, url: inject("url") }); + const domain = `docs-${Math.random()}.fern.com`; - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v1.write.startDocsRegister({ - orgId: FdrAPI.OrgId("fern"), - domain, - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - }), - ); - await fdr.docs.v1.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, - }); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v1.write.startDocsRegister({ + orgId: FdrAPI.OrgId("fern"), + domain, + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + }) + ); + await fdr.docs.v1.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: WRITE_DOCS_REGISTER_DEFINITION, + } + ); - const response = await fdr.docsCache.invalidate({ - url: FdrAPI.Url(`https://${domain}`), - }); + const response = await fdr.docsCache.invalidate({ + url: FdrAPI.Url(`https://${domain}`), + }); - expect(response.ok).toEqual(true); + expect(response.ok).toEqual(true); }); diff --git a/servers/fdr/src/__test__/local/services/git.test.ts b/servers/fdr/src/__test__/local/services/git.test.ts index ed6a3147d4..f6aa23dae4 100644 --- a/servers/fdr/src/__test__/local/services/git.test.ts +++ b/servers/fdr/src/__test__/local/services/git.test.ts @@ -3,25 +3,27 @@ import { inject } from "vitest"; import { getAPIResponse, getClient } from "../util"; it("register repo", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - await fdr.git.upsertRepository({ - type: "config", - id: { - type: "github", - id: "test", - }, - name: "name", - owner: "owner", - fullName: "repository.full_name", - url: FdrAPI.Url("repository.html_url"), - repositoryOwnerOrganizationId: FdrAPI.OrgId("organizationId"), - defaultBranchChecks: [], - }); + const fdr = getClient({ authed: true, url: inject("url") }); + await fdr.git.upsertRepository({ + type: "config", + id: { + type: "github", + id: "test", + }, + name: "name", + owner: "owner", + fullName: "repository.full_name", + url: FdrAPI.Url("repository.html_url"), + repositoryOwnerOrganizationId: FdrAPI.OrgId("organizationId"), + defaultBranchChecks: [], + }); - const registeredRepo = getAPIResponse(await fdr.git.getRepository("owner", "name")); + const registeredRepo = getAPIResponse( + await fdr.git.getRepository("owner", "name") + ); - expect(registeredRepo.id).toEqual({ - type: "github", - id: "test", - }); + expect(registeredRepo.id).toEqual({ + type: "github", + id: "test", + }); }); diff --git a/servers/fdr/src/__test__/local/services/ownership.test.ts b/servers/fdr/src/__test__/local/services/ownership.test.ts index 50e8f04217..93c6579377 100644 --- a/servers/fdr/src/__test__/local/services/ownership.test.ts +++ b/servers/fdr/src/__test__/local/services/ownership.test.ts @@ -3,34 +3,42 @@ import { inject } from "vitest"; import { getClient } from "../util"; it("change domain ownership", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); + const fdr = getClient({ authed: true, url: inject("url") }); - const domain = `docs-${Math.random()}.fern.com`; + const domain = `docs-${Math.random()}.fern.com`; - // register docs - await fdr.docs.v1.write.startDocsRegister({ - orgId: FdrAPI.OrgId("fern"), - domain, - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - }); + // register docs + await fdr.docs.v1.write.startDocsRegister({ + orgId: FdrAPI.OrgId("fern"), + domain, + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + }); - const response = await fdr.docs.v2.write.transferOwnershipOfDomain({ - domain, - toOrgId: FdrAPI.OrgId("acme"), - }); + const response = await fdr.docs.v2.write.transferOwnershipOfDomain({ + domain, + toOrgId: FdrAPI.OrgId("acme"), + }); - if (!response.ok) { - throw new Error(`Failed to transfer ownership of domain: ${response.error}`); - } - expect(response.ok).toBe(true); + if (!response.ok) { + throw new Error( + `Failed to transfer ownership of domain: ${response.error}` + ); + } + expect(response.ok).toBe(true); - // verify ownership - const registerResponse = await fdr.docs.v1.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - domain, - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - }); - if (!registerResponse.ok) { - throw new Error(`Failed to reregister domain: ${registerResponse.error}`); - } + // verify ownership + const registerResponse = await fdr.docs.v1.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + domain, + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + }); + if (!registerResponse.ok) { + throw new Error(`Failed to reregister domain: ${registerResponse.error}`); + } }); diff --git a/servers/fdr/src/__test__/local/services/snippetTemplates.test.ts b/servers/fdr/src/__test__/local/services/snippetTemplates.test.ts index 5e08f5e6aa..eaca99c6be 100644 --- a/servers/fdr/src/__test__/local/services/snippetTemplates.test.ts +++ b/servers/fdr/src/__test__/local/services/snippetTemplates.test.ts @@ -5,161 +5,161 @@ import { CHAT_COMPLETION_PAYLOAD, CHAT_COMPLETION_SNIPPET } from "../../octo"; import { getAPIResponse, getClient } from "../util"; const ENDPOINT: FdrAPI.EndpointIdentifier = { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: "GET", - identifierOverride: undefined, + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: "GET", + identifierOverride: undefined, }; const SDK: FdrAPI.Sdk = { - type: "go", - githubRepo: "https://github.com/users-api/users-go", - version: "0.0.15", + type: "go", + githubRepo: "https://github.com/users-api/users-go", + version: "0.0.15", }; it("create snippet template", async () => { - const unauthedFdr = getClient({ authed: false, url: inject("url") }); - const fdr = getClient({ authed: true, url: inject("url") }); + const unauthedFdr = getClient({ authed: false, url: inject("url") }); + const fdr = getClient({ authed: true, url: inject("url") }); - const orgId = FdrAPI.OrgId("acme"); + const orgId = FdrAPI.OrgId("acme"); - // register API definition for acme org - await unauthedFdr.templates.register({ - orgId, - apiId: FdrAPI.ApiId("user"), - apiDefinitionId: FdrAPI.ApiDefinitionId("...."), - snippet: { - endpointId: ENDPOINT, - sdk: SDK, - snippetTemplate: { - type: "v1", - clientInstantiation: "client := userclient.New()", - functionInvocation: { - type: "generic", - templateString: "client.GetUsers()", - isOptional: false, - imports: undefined, - templateInputs: undefined, - inputDelimiter: undefined, - }, - }, - additionalTemplates: undefined, + // register API definition for acme org + await unauthedFdr.templates.register({ + orgId, + apiId: FdrAPI.ApiId("user"), + apiDefinitionId: FdrAPI.ApiDefinitionId("...."), + snippet: { + endpointId: ENDPOINT, + sdk: SDK, + snippetTemplate: { + type: "v1", + clientInstantiation: "client := userclient.New()", + functionInvocation: { + type: "generic", + templateString: "client.GetUsers()", + isOptional: false, + imports: undefined, + templateInputs: undefined, + inputDelimiter: undefined, }, - }); - // create snippets - const response = await fdr.templates.get({ - orgId, - apiId: FdrAPI.ApiId("user"), - endpointId: ENDPOINT, - sdk: SDK, - }); - console.log(JSON.stringify(response, null, 2)); - expect(response.ok).toBe(true); - if (!response.ok) { - throw new Error("Failed to load snippet template"); - } - expect(response.body.endpointId).toEqual(ENDPOINT); + }, + additionalTemplates: undefined, + }, + }); + // create snippets + const response = await fdr.templates.get({ + orgId, + apiId: FdrAPI.ApiId("user"), + endpointId: ENDPOINT, + sdk: SDK, + }); + console.log(JSON.stringify(response, null, 2)); + expect(response.ok).toBe(true); + if (!response.ok) { + throw new Error("Failed to load snippet template"); + } + expect(response.body.endpointId).toEqual(ENDPOINT); }); it("generate example from snippet template", async () => { - const unauthedFdr = getClient({ authed: false, url: inject("url") }); - const fdr = getClient({ authed: true, url: inject("url") }); + const unauthedFdr = getClient({ authed: false, url: inject("url") }); + const fdr = getClient({ authed: true, url: inject("url") }); - const orgId = FdrAPI.OrgId("octoai"); - const apiId = FdrAPI.ApiId("api"); - const sdk: FernRegistry.Sdk = { - type: "python", - package: "octoai", - version: "0.0.5", - }; + const orgId = FdrAPI.OrgId("octoai"); + const apiId = FdrAPI.ApiId("api"); + const sdk: FernRegistry.Sdk = { + type: "python", + package: "octoai", + version: "0.0.5", + }; - // register API definition for acme org - await unauthedFdr.templates.register({ - orgId, - apiId, - apiDefinitionId: FdrAPI.ApiDefinitionId("...."), - snippet: CHAT_COMPLETION_SNIPPET("0.0.5"), - }); - // create snippets - await fdr.templates.get({ - orgId, - apiId, - endpointId: CHAT_COMPLETION_SNIPPET("0.0.5").endpointId, - sdk, - }); + // register API definition for acme org + await unauthedFdr.templates.register({ + orgId, + apiId, + apiDefinitionId: FdrAPI.ApiDefinitionId("...."), + snippet: CHAT_COMPLETION_SNIPPET("0.0.5"), + }); + // create snippets + await fdr.templates.get({ + orgId, + apiId, + endpointId: CHAT_COMPLETION_SNIPPET("0.0.5").endpointId, + sdk, + }); - const response = await fdr.snippets.get({ - orgId, - apiId, - endpoint: CHAT_COMPLETION_SNIPPET("0.0.5").endpointId, - sdks: [sdk], - payload: CHAT_COMPLETION_PAYLOAD, - }); - expect(response.ok).toBe(true); - console.log(JSON.stringify(response, null, 2)); + const response = await fdr.snippets.get({ + orgId, + apiId, + endpoint: CHAT_COMPLETION_SNIPPET("0.0.5").endpointId, + sdks: [sdk], + payload: CHAT_COMPLETION_PAYLOAD, + }); + expect(response.ok).toBe(true); + console.log(JSON.stringify(response, null, 2)); }); it("fallback to version", async () => { - const unauthedFdr = getClient({ authed: false, url: inject("url") }); - const fdr = getClient({ authed: true, url: inject("url") }); + const unauthedFdr = getClient({ authed: false, url: inject("url") }); + const fdr = getClient({ authed: true, url: inject("url") }); - const orgId = FdrAPI.OrgId("octoai"); - const apiId = FdrAPI.ApiId("api"); - const sdk: FernRegistry.Sdk = { - type: "python", - package: "octoai", - version: "0.0.6", - }; - const genericRequest: FernRegistry.SdkRequest = { - type: "python", - package: "octoai", - version: undefined, - }; + const orgId = FdrAPI.OrgId("octoai"); + const apiId = FdrAPI.ApiId("api"); + const sdk: FernRegistry.Sdk = { + type: "python", + package: "octoai", + version: "0.0.6", + }; + const genericRequest: FernRegistry.SdkRequest = { + type: "python", + package: "octoai", + version: undefined, + }; - // register API definition for acme org - const reg = await unauthedFdr.templates.register({ - orgId, - apiId, - apiDefinitionId: FdrAPI.ApiDefinitionId("...."), - snippet: CHAT_COMPLETION_SNIPPET("0.0.6"), - }); - expect(reg.ok).toBe(true); - // create snippets - const template = getAPIResponse( - await fdr.templates.get({ - orgId, - apiId, - endpointId: CHAT_COMPLETION_SNIPPET("0.0.6").endpointId, - sdk: genericRequest, - }), - ); - expect(template.sdk.version).toBe("0.0.6"); + // register API definition for acme org + const reg = await unauthedFdr.templates.register({ + orgId, + apiId, + apiDefinitionId: FdrAPI.ApiDefinitionId("...."), + snippet: CHAT_COMPLETION_SNIPPET("0.0.6"), + }); + expect(reg.ok).toBe(true); + // create snippets + const template = getAPIResponse( + await fdr.templates.get({ + orgId, + apiId, + endpointId: CHAT_COMPLETION_SNIPPET("0.0.6").endpointId, + sdk: genericRequest, + }) + ); + expect(template.sdk.version).toBe("0.0.6"); - // register API definition for acme org - const regAgain = await unauthedFdr.templates.register({ - orgId, - apiId, - apiDefinitionId: FdrAPI.ApiDefinitionId("...."), - snippet: CHAT_COMPLETION_SNIPPET("0.0.122"), - }); - expect(regAgain.ok).toBe(true); - // create snippets - const templateAgain = getAPIResponse( - await fdr.templates.get({ - orgId, - apiId, - endpointId: CHAT_COMPLETION_SNIPPET("0.0.122").endpointId, - sdk: genericRequest, - }), - ); - expect(templateAgain.sdk.version).toBe("0.0.122"); + // register API definition for acme org + const regAgain = await unauthedFdr.templates.register({ + orgId, + apiId, + apiDefinitionId: FdrAPI.ApiDefinitionId("...."), + snippet: CHAT_COMPLETION_SNIPPET("0.0.122"), + }); + expect(regAgain.ok).toBe(true); + // create snippets + const templateAgain = getAPIResponse( + await fdr.templates.get({ + orgId, + apiId, + endpointId: CHAT_COMPLETION_SNIPPET("0.0.122").endpointId, + sdk: genericRequest, + }) + ); + expect(templateAgain.sdk.version).toBe("0.0.122"); - const templateSpecify = await fdr.templates.get({ - orgId, - apiId, - endpointId: CHAT_COMPLETION_SNIPPET("0.0.6").endpointId, - sdk, - }); - expect(templateSpecify.ok).toBe(true); - if (templateSpecify.ok) { - expect(templateSpecify.body.sdk.version).toBe("0.0.6"); - } + const templateSpecify = await fdr.templates.get({ + orgId, + apiId, + endpointId: CHAT_COMPLETION_SNIPPET("0.0.6").endpointId, + sdk, + }); + expect(templateSpecify.ok).toBe(true); + if (templateSpecify.ok) { + expect(templateSpecify.body.sdk.version).toBe("0.0.6"); + } }); diff --git a/servers/fdr/src/__test__/local/services/snippets.test.ts b/servers/fdr/src/__test__/local/services/snippets.test.ts index e330d9d3cf..aae1685e74 100644 --- a/servers/fdr/src/__test__/local/services/snippets.test.ts +++ b/servers/fdr/src/__test__/local/services/snippets.test.ts @@ -6,751 +6,810 @@ import { EMPTY_REGISTER_API_DEFINITION } from "./api.test"; import { FONT_FILE_ID } from "./docs.test"; it("get snippets", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("foo"), - snippets: { - type: "python", - sdk: { - package: "acme", - version: "0.0.2", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/snippets/load"), - method: FdrAPI.HttpMethod.Post, - identifierOverride: undefined, - }, - snippet: { - async_client: "const petstore = new AsyncPetstoreClient(\napi_key='YOUR_API_KEY',\n)", - sync_client: "const petstore = new PetstoreClient(\napi_key='YOUR_API_KEY',\n)", - }, - exampleIdentifier: undefined, - }, - ], + const fdr = getClient({ authed: true, url: inject("url") }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("foo"), + snippets: { + type: "python", + sdk: { + package: "acme", + version: "0.0.2", + }, + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/snippets/load"), + method: FdrAPI.HttpMethod.Post, + identifierOverride: undefined, + }, + snippet: { + async_client: + "const petstore = new AsyncPetstoreClient(\napi_key='YOUR_API_KEY',\n)", + sync_client: + "const petstore = new PetstoreClient(\napi_key='YOUR_API_KEY',\n)", + }, + exampleIdentifier: undefined, }, - }); - // get snippets - const snippets = getAPIResponse( - await fdr.snippets.get({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("foo"), - endpoint: { - path: FdrAPI.EndpointPathLiteral("/snippets/load"), - method: FdrAPI.HttpMethod.Post, - identifierOverride: undefined, - }, - }), - ); - expect(snippets.length).toEqual(1); + ], + }, + }); + // get snippets + const snippets = getAPIResponse( + await fdr.snippets.get({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("foo"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/snippets/load"), + method: FdrAPI.HttpMethod.Post, + identifierOverride: undefined, + }, + }) + ); + expect(snippets.length).toEqual(1); - const snippet = snippets[0] as FdrAPI.PythonSnippet; - expect(snippet.sdk.package).toEqual("acme"); - expect(snippet.sdk.version).toEqual("0.0.2"); - expect(snippet.async_client).toEqual("const petstore = new AsyncPetstoreClient(\napi_key='YOUR_API_KEY',\n)"); - expect(snippet.sync_client).toEqual("const petstore = new PetstoreClient(\napi_key='YOUR_API_KEY',\n)"); - // register API definition for acme org - const apiDefinitionResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("foo"), - definition: createApiDefinition({ - endpointId: FdrAPI.EndpointId("/snippets/load"), - endpointMethod: "POST", - endpointPath: { - parts: [ - { type: "literal", value: "/snippets" }, - { type: "literal", value: "/load" }, - ], - pathParameters: [], - }, - snippetsConfig: { - pythonSdk: { - package: "acme", - version: undefined, - }, - typescriptSdk: undefined, - goSdk: undefined, - rubySdk: undefined, - javaSdk: undefined, - }, - }), - }), - ); - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("foo"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: [], - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - images: [], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: { - pages: {}, - config: { - navigation: { - landingPage: undefined, - items: [ - { - type: "api", - title: "Acme API", - api: apiDefinitionResponse.apiDefinitionId, - artifacts: undefined, - skipUrlSlug: undefined, - showErrors: undefined, - changelog: undefined, - changelogV2: undefined, - navigation: undefined, - longScrolling: undefined, - flattened: undefined, - icon: undefined, - hidden: undefined, - urlSlugOverride: undefined, - fullSlug: undefined, - }, - ], - }, - root: undefined, - typography: { - headingsFont: { - name: "Syne", - fontFile: FONT_FILE_ID, - }, - bodyFont: undefined, - codeFont: undefined, - }, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - backgroundImage: undefined, - logoV2: undefined, - logo: undefined, - colors: undefined, - colorsV2: undefined, + const snippet = snippets[0] as FdrAPI.PythonSnippet; + expect(snippet.sdk.package).toEqual("acme"); + expect(snippet.sdk.version).toEqual("0.0.2"); + expect(snippet.async_client).toEqual( + "const petstore = new AsyncPetstoreClient(\napi_key='YOUR_API_KEY',\n)" + ); + expect(snippet.sync_client).toEqual( + "const petstore = new PetstoreClient(\napi_key='YOUR_API_KEY',\n)" + ); + // register API definition for acme org + const apiDefinitionResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("foo"), + definition: createApiDefinition({ + endpointId: FdrAPI.EndpointId("/snippets/load"), + endpointMethod: "POST", + endpointPath: { + parts: [ + { type: "literal", value: "/snippets" }, + { type: "literal", value: "/load" }, + ], + pathParameters: [], + }, + snippetsConfig: { + pythonSdk: { + package: "acme", + version: undefined, + }, + typescriptSdk: undefined, + goSdk: undefined, + rubySdk: undefined, + javaSdk: undefined, + }, + }), + }) + ); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("foo"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: [], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + images: [], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: { + pages: {}, + config: { + navigation: { + landingPage: undefined, + items: [ + { + type: "api", + title: "Acme API", + api: apiDefinitionResponse.apiDefinitionId, + artifacts: undefined, + skipUrlSlug: undefined, + showErrors: undefined, + changelog: undefined, + changelogV2: undefined, + navigation: undefined, + longScrolling: undefined, + flattened: undefined, + icon: undefined, + hidden: undefined, + urlSlugOverride: undefined, + fullSlug: undefined, + }, + ], + }, + root: undefined, + typography: { + headingsFont: { + name: "Syne", + fontFile: FONT_FILE_ID, }, - jsFiles: undefined, + bodyFont: undefined, + codeFont: undefined, + }, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + backgroundImage: undefined, + logoV2: undefined, + logo: undefined, + colors: undefined, + colorsV2: undefined, }, - }); - // get docs for url - const docs = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), - }), - ); - const apiDefinition = docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; - expect(apiDefinition).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.pythonSdk).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.pythonSdk?.async_client).toEqual( - "const petstore = new AsyncPetstoreClient(\napi_key='YOUR_API_KEY',\n)", - ); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.pythonSdk?.sync_client).toEqual( - "const petstore = new PetstoreClient(\napi_key='YOUR_API_KEY',\n)", - ); + jsFiles: undefined, + }, + } + ); + // get docs for url + const docs = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), + }) + ); + const apiDefinition = + docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; + expect(apiDefinition).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.pythonSdk + ).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.pythonSdk + ?.async_client + ).toEqual( + "const petstore = new AsyncPetstoreClient(\napi_key='YOUR_API_KEY',\n)" + ); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.pythonSdk + ?.sync_client + ).toEqual("const petstore = new PetstoreClient(\napi_key='YOUR_API_KEY',\n)"); }); it("get Go snippets", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("echo"), - snippets: { - type: "go", - sdk: { - githubRepo: "https://github.com/acme/acme-go", - version: "0.0.10", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/snippets/load"), - method: FdrAPI.HttpMethod.Post, - identifierOverride: undefined, - }, - snippet: { - client: "client := acmeclient.NewClient()\n", - }, - exampleIdentifier: undefined, - }, - ], + const fdr = getClient({ authed: true, url: inject("url") }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("echo"), + snippets: { + type: "go", + sdk: { + githubRepo: "https://github.com/acme/acme-go", + version: "0.0.10", + }, + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/snippets/load"), + method: FdrAPI.HttpMethod.Post, + identifierOverride: undefined, + }, + snippet: { + client: "client := acmeclient.NewClient()\n", + }, + exampleIdentifier: undefined, }, - }); - // get snippets - const snippets = getAPIResponse( - await fdr.snippets.get({ - apiId: FdrAPI.ApiId("echo"), - orgId: FdrAPI.OrgId("acme"), - endpoint: { - path: FdrAPI.EndpointPathLiteral("/snippets/load"), - method: FdrAPI.HttpMethod.Post, - identifierOverride: undefined, - }, - }), - ); - expect(snippets.length).toEqual(1); + ], + }, + }); + // get snippets + const snippets = getAPIResponse( + await fdr.snippets.get({ + apiId: FdrAPI.ApiId("echo"), + orgId: FdrAPI.OrgId("acme"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/snippets/load"), + method: FdrAPI.HttpMethod.Post, + identifierOverride: undefined, + }, + }) + ); + expect(snippets.length).toEqual(1); - const snippet = snippets[0] as FdrAPI.GoSnippet; - expect(snippet.sdk.githubRepo).toEqual("https://github.com/acme/acme-go"); - expect(snippet.sdk.version).toEqual("0.0.10"); - expect(snippet.client).toEqual("client := acmeclient.NewClient()\n"); - // register API definition for acme org - const apiDefinitionResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("echo"), - definition: createApiDefinition({ - endpointId: FdrAPI.EndpointId("/snippets/load"), - endpointMethod: "POST", - endpointPath: { - parts: [ - { type: "literal", value: "/snippets" }, - { type: "literal", value: "/load" }, - ], - pathParameters: [], - }, - snippetsConfig: { - goSdk: { - githubRepo: "https://github.com/acme/acme-go", - version: undefined, - }, - typescriptSdk: undefined, - pythonSdk: undefined, - javaSdk: undefined, - rubySdk: undefined, - }, - }), - }), - ); - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("echo"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: [], - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - images: [], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: { - pages: {}, - config: { - navigation: { - items: [ - { - type: "api", - title: "Acme API", - api: apiDefinitionResponse.apiDefinitionId, - artifacts: undefined, - skipUrlSlug: undefined, - showErrors: undefined, - changelog: undefined, - changelogV2: undefined, - navigation: undefined, - longScrolling: undefined, - flattened: undefined, - icon: undefined, - hidden: undefined, - urlSlugOverride: undefined, - fullSlug: undefined, - }, - ], - landingPage: undefined, - }, - root: undefined, - typography: { - headingsFont: { - name: "Syne", - fontFile: FONT_FILE_ID, - }, - bodyFont: undefined, - codeFont: undefined, - }, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - backgroundImage: undefined, - logoV2: undefined, - logo: undefined, - colors: undefined, - colorsV2: undefined, + const snippet = snippets[0] as FdrAPI.GoSnippet; + expect(snippet.sdk.githubRepo).toEqual("https://github.com/acme/acme-go"); + expect(snippet.sdk.version).toEqual("0.0.10"); + expect(snippet.client).toEqual("client := acmeclient.NewClient()\n"); + // register API definition for acme org + const apiDefinitionResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("echo"), + definition: createApiDefinition({ + endpointId: FdrAPI.EndpointId("/snippets/load"), + endpointMethod: "POST", + endpointPath: { + parts: [ + { type: "literal", value: "/snippets" }, + { type: "literal", value: "/load" }, + ], + pathParameters: [], + }, + snippetsConfig: { + goSdk: { + githubRepo: "https://github.com/acme/acme-go", + version: undefined, + }, + typescriptSdk: undefined, + pythonSdk: undefined, + javaSdk: undefined, + rubySdk: undefined, + }, + }), + }) + ); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("echo"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: [], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + images: [], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: { + pages: {}, + config: { + navigation: { + items: [ + { + type: "api", + title: "Acme API", + api: apiDefinitionResponse.apiDefinitionId, + artifacts: undefined, + skipUrlSlug: undefined, + showErrors: undefined, + changelog: undefined, + changelogV2: undefined, + navigation: undefined, + longScrolling: undefined, + flattened: undefined, + icon: undefined, + hidden: undefined, + urlSlugOverride: undefined, + fullSlug: undefined, + }, + ], + landingPage: undefined, + }, + root: undefined, + typography: { + headingsFont: { + name: "Syne", + fontFile: FONT_FILE_ID, }, - jsFiles: undefined, + bodyFont: undefined, + codeFont: undefined, + }, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + backgroundImage: undefined, + logoV2: undefined, + logo: undefined, + colors: undefined, + colorsV2: undefined, }, - }); - // get docs for url - const docs = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), - }), - ); - const apiDefinition = docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; - expect(apiDefinition).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk?.client).toEqual( - "client := acmeclient.NewClient()\n", - ); + jsFiles: undefined, + }, + } + ); + // get docs for url + const docs = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), + }) + ); + const apiDefinition = + docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; + expect(apiDefinition).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk + ).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk + ?.client + ).toEqual("client := acmeclient.NewClient()\n"); }); it("get Ruby snippets", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("bar"), - snippets: { - type: "ruby", - sdk: { - gem: "acme_ruby", - version: "0.0.10", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/snippets/load"), - method: FdrAPI.HttpMethod.Post, - identifierOverride: undefined, - }, - snippet: { - client: "client = Acme::Client()\n", - }, - exampleIdentifier: undefined, - }, - ], + const fdr = getClient({ authed: true, url: inject("url") }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("bar"), + snippets: { + type: "ruby", + sdk: { + gem: "acme_ruby", + version: "0.0.10", + }, + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/snippets/load"), + method: FdrAPI.HttpMethod.Post, + identifierOverride: undefined, + }, + snippet: { + client: "client = Acme::Client()\n", + }, + exampleIdentifier: undefined, }, - }); - // get snippets - const snippets = getAPIResponse( - await fdr.snippets.get({ - apiId: FdrAPI.ApiId("bar"), - orgId: FdrAPI.OrgId("acme"), - endpoint: { - path: FdrAPI.EndpointPathLiteral("/snippets/load"), - method: FdrAPI.HttpMethod.Post, - identifierOverride: undefined, - }, - }), - ); - expect(snippets.length).toEqual(1); + ], + }, + }); + // get snippets + const snippets = getAPIResponse( + await fdr.snippets.get({ + apiId: FdrAPI.ApiId("bar"), + orgId: FdrAPI.OrgId("acme"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/snippets/load"), + method: FdrAPI.HttpMethod.Post, + identifierOverride: undefined, + }, + }) + ); + expect(snippets.length).toEqual(1); - const snippet = snippets[0] as FdrAPI.RubySnippet; - expect(snippet.sdk.gem).toEqual("acme_ruby"); - expect(snippet.sdk.version).toEqual("0.0.10"); - expect(snippet.client).toEqual("client = Acme::Client()\n"); - // register API definition for acme org - const apiDefinitionResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("bar"), - definition: createApiDefinition({ - endpointId: FdrAPI.EndpointId("/snippets/load"), - endpointMethod: "POST", - endpointPath: { - parts: [ - { type: "literal", value: "/snippets" }, - { type: "literal", value: "/load" }, - ], - pathParameters: [], - }, - snippetsConfig: { - rubySdk: { - gem: "acme_ruby", - version: undefined, - }, - typescriptSdk: undefined, - pythonSdk: undefined, - javaSdk: undefined, - goSdk: undefined, - }, - }), - }), - ); - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("bar"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: [], - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - images: [], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: { - pages: {}, - config: { - navigation: { - items: [ - { - type: "api", - title: "Acme API", - api: apiDefinitionResponse.apiDefinitionId, - artifacts: undefined, - skipUrlSlug: undefined, - showErrors: undefined, - changelog: undefined, - changelogV2: undefined, - navigation: undefined, - longScrolling: undefined, - flattened: undefined, - icon: undefined, - hidden: undefined, - urlSlugOverride: undefined, - fullSlug: undefined, - }, - ], - landingPage: undefined, - }, - root: undefined, - typography: { - headingsFont: { - name: "Syne", - fontFile: FONT_FILE_ID, - }, - bodyFont: undefined, - codeFont: undefined, - }, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - backgroundImage: undefined, - logoV2: undefined, - logo: undefined, - colors: undefined, - colorsV2: undefined, + const snippet = snippets[0] as FdrAPI.RubySnippet; + expect(snippet.sdk.gem).toEqual("acme_ruby"); + expect(snippet.sdk.version).toEqual("0.0.10"); + expect(snippet.client).toEqual("client = Acme::Client()\n"); + // register API definition for acme org + const apiDefinitionResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("bar"), + definition: createApiDefinition({ + endpointId: FdrAPI.EndpointId("/snippets/load"), + endpointMethod: "POST", + endpointPath: { + parts: [ + { type: "literal", value: "/snippets" }, + { type: "literal", value: "/load" }, + ], + pathParameters: [], + }, + snippetsConfig: { + rubySdk: { + gem: "acme_ruby", + version: undefined, + }, + typescriptSdk: undefined, + pythonSdk: undefined, + javaSdk: undefined, + goSdk: undefined, + }, + }), + }) + ); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("bar"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: [], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + images: [], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: { + pages: {}, + config: { + navigation: { + items: [ + { + type: "api", + title: "Acme API", + api: apiDefinitionResponse.apiDefinitionId, + artifacts: undefined, + skipUrlSlug: undefined, + showErrors: undefined, + changelog: undefined, + changelogV2: undefined, + navigation: undefined, + longScrolling: undefined, + flattened: undefined, + icon: undefined, + hidden: undefined, + urlSlugOverride: undefined, + fullSlug: undefined, + }, + ], + landingPage: undefined, + }, + root: undefined, + typography: { + headingsFont: { + name: "Syne", + fontFile: FONT_FILE_ID, }, - jsFiles: undefined, + bodyFont: undefined, + codeFont: undefined, + }, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + backgroundImage: undefined, + logoV2: undefined, + logo: undefined, + colors: undefined, + colorsV2: undefined, }, - }); - // get docs for url - const docs = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), - }), - ); - const apiDefinition = docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; - expect(apiDefinition).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.rubySdk).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.rubySdk?.client).toEqual( - "client = Acme::Client()\n", - ); + jsFiles: undefined, + }, + } + ); + // get docs for url + const docs = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), + }) + ); + const apiDefinition = + docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; + expect(apiDefinition).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.rubySdk + ).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.rubySdk + ?.client + ).toEqual("client = Acme::Client()\n"); }); it("get snippets with unregistered API", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("fresh"), - snippets: { - type: "typescript", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - client: "const petstore = new PetstoreClient({\napiKey: 'YOUR_API_KEY',\n});", - }, - exampleIdentifier: undefined, - }, - ], + const fdr = getClient({ authed: true, url: inject("url") }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("fresh"), + snippets: { + type: "typescript", + sdk: { + package: "acme", + version: "0.0.1", + }, + snippets: [ + { + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + client: + "const petstore = new PetstoreClient({\napiKey: 'YOUR_API_KEY',\n});", + }, + exampleIdentifier: undefined, }, - }); - // get snippets - const snippets = getAPIResponse( - await fdr.snippets.get({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("fresh"), - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - }), - ); - expect(snippets.length).toEqual(1); + ], + }, + }); + // get snippets + const snippets = getAPIResponse( + await fdr.snippets.get({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("fresh"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + }) + ); + expect(snippets.length).toEqual(1); - const snippet = snippets[0] as FdrAPI.TypeScriptSnippet; - expect(snippet.sdk.package).toEqual("acme"); - expect(snippet.sdk.version).toEqual("0.0.1"); - expect(snippet.client).toEqual("const petstore = new PetstoreClient({\napiKey: 'YOUR_API_KEY',\n});"); + const snippet = snippets[0] as FdrAPI.TypeScriptSnippet; + expect(snippet.sdk.package).toEqual("acme"); + expect(snippet.sdk.version).toEqual("0.0.1"); + expect(snippet.client).toEqual( + "const petstore = new PetstoreClient({\napiKey: 'YOUR_API_KEY',\n});" + ); }); it("load snippets", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register API definition for acme org - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - definition: EMPTY_REGISTER_API_DEFINITION, - }); - // initialize enough snippets to occupy two pages - const snippets: FdrAPI.SingleTypescriptSnippetCreate[] = []; - for (let i = 0; i < DEFAULT_SNIPPETS_PAGE_SIZE * 2; i++) { - snippets.push({ - endpoint: { - path: FdrAPI.EndpointPathLiteral(`/users/v${i}`), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - client: `const clientV${i} = new UserClient({\napiKey: 'YOUR_API_KEY',\n});`, - }, - exampleIdentifier: undefined, - }); - } - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("petstore"), - snippets: { - type: "typescript", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets, - }, + const fdr = getClient({ authed: true, url: inject("url") }); + // register API definition for acme org + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + definition: EMPTY_REGISTER_API_DEFINITION, + }); + // initialize enough snippets to occupy two pages + const snippets: FdrAPI.SingleTypescriptSnippetCreate[] = []; + for (let i = 0; i < DEFAULT_SNIPPETS_PAGE_SIZE * 2; i++) { + snippets.push({ + endpoint: { + path: FdrAPI.EndpointPathLiteral(`/users/v${i}`), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + snippet: { + client: `const clientV${i} = new UserClient({\napiKey: 'YOUR_API_KEY',\n});`, + }, + exampleIdentifier: undefined, }); + } + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("petstore"), + snippets: { + type: "typescript", + sdk: { + package: "acme", + version: "0.0.1", + }, + snippets, + }, + }); - // load snippets (first page) - const firstResponse = getAPIResponse( - await fdr.snippets.load({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("petstore"), - }), - ); - expect(firstResponse.next).toEqual(2); - expect(Object.keys(firstResponse.snippets).length).toEqual(DEFAULT_SNIPPETS_PAGE_SIZE); + // load snippets (first page) + const firstResponse = getAPIResponse( + await fdr.snippets.load({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("petstore"), + }) + ); + expect(firstResponse.next).toEqual(2); + expect(Object.keys(firstResponse.snippets).length).toEqual( + DEFAULT_SNIPPETS_PAGE_SIZE + ); - for (let i = 0; i < DEFAULT_SNIPPETS_PAGE_SIZE; i++) { - const snippetsForEndpointMethod = firstResponse.snippets[FdrAPI.EndpointPathLiteral(`/users/v${i}`)]; - const responseSnippets = snippetsForEndpointMethod?.GET; - expect(responseSnippets?.length).toEqual(1); - if (responseSnippets === undefined) { - throw new Error("response snippets must not be undefined"); - } - const snippet = responseSnippets[0] as FdrAPI.TypeScriptSnippet; - expect(snippet.sdk.package).toEqual("acme"); - expect(snippet.sdk.version).toEqual("0.0.1"); - expect(snippet.client).toEqual(`const clientV${i} = new UserClient({\napiKey: 'YOUR_API_KEY',\n});`); + for (let i = 0; i < DEFAULT_SNIPPETS_PAGE_SIZE; i++) { + const snippetsForEndpointMethod = + firstResponse.snippets[FdrAPI.EndpointPathLiteral(`/users/v${i}`)]; + const responseSnippets = snippetsForEndpointMethod?.GET; + expect(responseSnippets?.length).toEqual(1); + if (responseSnippets === undefined) { + throw new Error("response snippets must not be undefined"); } - - // load snippets (second page) - const secondResponse = getAPIResponse( - await fdr.snippets.load({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("petstore"), - sdks: [ - { - type: "typescript", - package: "acme", - version: "0.0.1", - }, - ], - page: firstResponse.next, - }), + const snippet = responseSnippets[0] as FdrAPI.TypeScriptSnippet; + expect(snippet.sdk.package).toEqual("acme"); + expect(snippet.sdk.version).toEqual("0.0.1"); + expect(snippet.client).toEqual( + `const clientV${i} = new UserClient({\napiKey: 'YOUR_API_KEY',\n});` ); - expect(Object.keys(secondResponse.snippets).length).toEqual(DEFAULT_SNIPPETS_PAGE_SIZE); + } - for (let i = DEFAULT_SNIPPETS_PAGE_SIZE; i < DEFAULT_SNIPPETS_PAGE_SIZE * 2; i++) { - const snippetsForEndpointMethod = secondResponse.snippets[FdrAPI.EndpointPathLiteral(`/users/v${i}`)]; - const responseSnippets = snippetsForEndpointMethod?.GET; - expect(responseSnippets?.length).toEqual(1); - if (responseSnippets === undefined) { - throw new Error("response snippets must not be undefined"); - } - const snippet = responseSnippets[0] as FdrAPI.TypeScriptSnippet; - expect(snippet.sdk.package).toEqual("acme"); - expect(snippet.sdk.version).toEqual("0.0.1"); - expect(snippet.client).toEqual(`const clientV${i} = new UserClient({\napiKey: 'YOUR_API_KEY',\n});`); + // load snippets (second page) + const secondResponse = getAPIResponse( + await fdr.snippets.load({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("petstore"), + sdks: [ + { + type: "typescript", + package: "acme", + version: "0.0.1", + }, + ], + page: firstResponse.next, + }) + ); + expect(Object.keys(secondResponse.snippets).length).toEqual( + DEFAULT_SNIPPETS_PAGE_SIZE + ); + + for ( + let i = DEFAULT_SNIPPETS_PAGE_SIZE; + i < DEFAULT_SNIPPETS_PAGE_SIZE * 2; + i++ + ) { + const snippetsForEndpointMethod = + secondResponse.snippets[FdrAPI.EndpointPathLiteral(`/users/v${i}`)]; + const responseSnippets = snippetsForEndpointMethod?.GET; + expect(responseSnippets?.length).toEqual(1); + if (responseSnippets === undefined) { + throw new Error("response snippets must not be undefined"); } - expect(secondResponse.next).toEqual(3); + const snippet = responseSnippets[0] as FdrAPI.TypeScriptSnippet; + expect(snippet.sdk.package).toEqual("acme"); + expect(snippet.sdk.version).toEqual("0.0.1"); + expect(snippet.client).toEqual( + `const clientV${i} = new UserClient({\napiKey: 'YOUR_API_KEY',\n});` + ); + } + expect(secondResponse.next).toEqual(3); }); it("user not part of org", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("private"), - apiId: FdrAPI.ApiId("baz"), - snippets: { - type: "go", - sdk: { - githubRepo: "fern-api/user-go", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - client: "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", - }, - exampleIdentifier: undefined, - }, - ], - }, - }); - // get snippets - const response = await fdr.snippets.get({ - orgId: FdrAPI.OrgId("private"), - endpoint: { + const fdr = getClient({ authed: true, url: inject("url") }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("private"), + apiId: FdrAPI.ApiId("baz"), + snippets: { + type: "go", + sdk: { + githubRepo: "fern-api/user-go", + version: "0.0.1", + }, + snippets: [ + { + endpoint: { path: FdrAPI.EndpointPathLiteral("/users/v1"), method: FdrAPI.HttpMethod.Get, identifierOverride: undefined, + }, + snippet: { + client: + "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", + }, + exampleIdentifier: undefined, }, - }); - console.log("bruh", JSON.stringify(response)); + ], + }, + }); + // get snippets + const response = await fdr.snippets.get({ + orgId: FdrAPI.OrgId("private"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + }); + console.log("bruh", JSON.stringify(response)); - expect(!response.ok).toBe(true); + expect(!response.ok).toBe(true); }); it("snippets apiId not found", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("petstore"), - snippets: { - type: "typescript", - sdk: { - package: "acme", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - client: "const acme = new AcmeClient({\napiKey: 'YOUR_API_KEY',\n});", - }, - exampleIdentifier: undefined, - }, - ], - }, - }); - - // get not found apiId - const response = await fdr.snippets.get({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("dne"), - endpoint: { + const fdr = getClient({ authed: true, url: inject("url") }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("petstore"), + snippets: { + type: "typescript", + sdk: { + package: "acme", + version: "0.0.1", + }, + snippets: [ + { + endpoint: { path: FdrAPI.EndpointPathLiteral("/users/v1"), method: FdrAPI.HttpMethod.Get, identifierOverride: undefined, + }, + snippet: { + client: + "const acme = new AcmeClient({\napiKey: 'YOUR_API_KEY',\n});", + }, + exampleIdentifier: undefined, }, - }); - expect(!response.ok).toBe(true); + ], + }, + }); + + // get not found apiId + const response = await fdr.snippets.get({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("dne"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + }); + expect(!response.ok).toBe(true); }); it("get snippets (unauthenticated)", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register API definition for acme org - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - definition: EMPTY_REGISTER_API_DEFINITION, - }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - snippets: { - type: "go", - sdk: { - githubRepo: "fern-api/user-go", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("/users/v1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: undefined, - }, - snippet: { - client: "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", - }, - exampleIdentifier: undefined, - }, - ], - }, - }); - // get snippets - const unauthedFdr = getClient({ authed: false, url: inject("url") }); - const response = await unauthedFdr.snippets.get({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - endpoint: { + const fdr = getClient({ authed: true, url: inject("url") }); + // register API definition for acme org + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + definition: EMPTY_REGISTER_API_DEFINITION, + }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + snippets: { + type: "go", + sdk: { + githubRepo: "fern-api/user-go", + version: "0.0.1", + }, + snippets: [ + { + endpoint: { path: FdrAPI.EndpointPathLiteral("/users/v1"), method: FdrAPI.HttpMethod.Get, identifierOverride: undefined, + }, + snippet: { + client: + "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", + }, + exampleIdentifier: undefined, }, - }); - expect(response.ok).toBe(false); + ], + }, + }); + // get snippets + const unauthedFdr = getClient({ authed: false, url: inject("url") }); + const response = await unauthedFdr.snippets.get({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("/users/v1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: undefined, + }, + }); + expect(response.ok).toBe(false); }); diff --git a/servers/fdr/src/__test__/local/services/snippetsByEndpointId.test.ts b/servers/fdr/src/__test__/local/services/snippetsByEndpointId.test.ts index f26d046bc3..52958b7dbc 100644 --- a/servers/fdr/src/__test__/local/services/snippetsByEndpointId.test.ts +++ b/servers/fdr/src/__test__/local/services/snippetsByEndpointId.test.ts @@ -4,151 +4,164 @@ import { createApiDefinition, getAPIResponse, getClient } from "../util"; import { EMPTY_REGISTER_API_DEFINITION } from "./api.test"; it("Load snippets by endpoint id", async () => { - const fdr = getClient({ authed: true, url: inject("url") }); - // register API definition for acme org - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - definition: EMPTY_REGISTER_API_DEFINITION, - }); - // create snippets - await fdr.snippetsFactory.createSnippetsForSdk({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - snippets: { - type: "go", - sdk: { - githubRepo: "fern-api/user-go", - version: "0.0.1", - }, - snippets: [ - { - endpoint: { - path: FdrAPI.EndpointPathLiteral("$1"), - method: FdrAPI.HttpMethod.Get, - identifierOverride: "endpoint_users.list", - }, - snippet: { - client: "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", - }, - exampleIdentifier: undefined, - }, - ], - }, - }); - - const response = await fdr.snippets.get({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - endpoint: { + const fdr = getClient({ authed: true, url: inject("url") }); + // register API definition for acme org + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + definition: EMPTY_REGISTER_API_DEFINITION, + }); + // create snippets + await fdr.snippetsFactory.createSnippetsForSdk({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + snippets: { + type: "go", + sdk: { + githubRepo: "fern-api/user-go", + version: "0.0.1", + }, + snippets: [ + { + endpoint: { path: FdrAPI.EndpointPathLiteral("$1"), method: FdrAPI.HttpMethod.Get, identifierOverride: "endpoint_users.list", + }, + snippet: { + client: + "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", + }, + exampleIdentifier: undefined, }, - }); - expect(response.ok).toBe(true); + ], + }, + }); + + const response = await fdr.snippets.get({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + endpoint: { + path: FdrAPI.EndpointPathLiteral("$1"), + method: FdrAPI.HttpMethod.Get, + identifierOverride: "endpoint_users.list", + }, + }); + expect(response.ok).toBe(true); - // register API definition for acme org - const apiDefinitionResponse = getAPIResponse( - await fdr.api.v1.register.registerApiDefinition({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - definition: createApiDefinition({ - endpointId: FdrAPI.EndpointId("list"), - originalEndpointId: "endpoint_users.list", - endpointMethod: "POST", // intentionally post to make sure that match happens from originalEndpointId - endpointPath: { - parts: [{ type: "literal", value: "/users/v1" }], - pathParameters: [], - }, - snippetsConfig: { - goSdk: { - githubRepo: "fern-api/user-go", - version: "0.0.1", - }, - typescriptSdk: undefined, - pythonSdk: undefined, - javaSdk: undefined, - rubySdk: undefined, - }, - }), - }), - ); - // register docs - const startDocsRegisterResponse = getAPIResponse( - await fdr.docs.v2.write.startDocsRegister({ - orgId: FdrAPI.OrgId("acme"), - apiId: FdrAPI.ApiId("user"), - domain: "https://acme.docs.buildwithfern.com", - customDomains: [], - filepaths: [DocsV1Write.FilePath("logo.png"), DocsV1Write.FilePath("guides/guide.mdx")], - images: [], - }), - ); - await fdr.docs.v2.write.finishDocsRegister(startDocsRegisterResponse.docsRegistrationId, { - docsDefinition: { - pages: {}, - config: { - navigation: { - landingPage: undefined, - items: [ - { - type: "api", - title: "Acme API", - api: apiDefinitionResponse.apiDefinitionId, - artifacts: undefined, - skipUrlSlug: undefined, - showErrors: false, - changelog: undefined, - changelogV2: undefined, - navigation: undefined, - longScrolling: undefined, - flattened: undefined, - icon: undefined, - hidden: undefined, - urlSlugOverride: undefined, - fullSlug: undefined, - } satisfies DocsV1Write.NavigationItem, - ], - }, - root: undefined, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - backgroundImage: undefined, - logoV2: undefined, - logo: undefined, - colors: undefined, - colorsV2: undefined, - typography: undefined, - }, - jsFiles: undefined, + // register API definition for acme org + const apiDefinitionResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + definition: createApiDefinition({ + endpointId: FdrAPI.EndpointId("list"), + originalEndpointId: "endpoint_users.list", + endpointMethod: "POST", // intentionally post to make sure that match happens from originalEndpointId + endpointPath: { + parts: [{ type: "literal", value: "/users/v1" }], + pathParameters: [], + }, + snippetsConfig: { + goSdk: { + githubRepo: "fern-api/user-go", + version: "0.0.1", + }, + typescriptSdk: undefined, + pythonSdk: undefined, + javaSdk: undefined, + rubySdk: undefined, + }, + }), + }) + ); + // register docs + const startDocsRegisterResponse = getAPIResponse( + await fdr.docs.v2.write.startDocsRegister({ + orgId: FdrAPI.OrgId("acme"), + apiId: FdrAPI.ApiId("user"), + domain: "https://acme.docs.buildwithfern.com", + customDomains: [], + filepaths: [ + DocsV1Write.FilePath("logo.png"), + DocsV1Write.FilePath("guides/guide.mdx"), + ], + images: [], + }) + ); + await fdr.docs.v2.write.finishDocsRegister( + startDocsRegisterResponse.docsRegistrationId, + { + docsDefinition: { + pages: {}, + config: { + navigation: { + landingPage: undefined, + items: [ + { + type: "api", + title: "Acme API", + api: apiDefinitionResponse.apiDefinitionId, + artifacts: undefined, + skipUrlSlug: undefined, + showErrors: false, + changelog: undefined, + changelogV2: undefined, + navigation: undefined, + longScrolling: undefined, + flattened: undefined, + icon: undefined, + hidden: undefined, + urlSlugOverride: undefined, + fullSlug: undefined, + } satisfies DocsV1Write.NavigationItem, + ], + }, + root: undefined, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + backgroundImage: undefined, + logoV2: undefined, + logo: undefined, + colors: undefined, + colorsV2: undefined, + typography: undefined, }, - }); - // get docs for url - const docs = getAPIResponse( - await fdr.docs.v2.read.getDocsForUrl({ - url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), - }), - ); - const apiDefinition = docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; - expect(apiDefinition).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk).not.toEqual(undefined); - expect(apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk?.client).toEqual( - "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')", - ); + jsFiles: undefined, + }, + } + ); + // get docs for url + const docs = getAPIResponse( + await fdr.docs.v2.read.getDocsForUrl({ + url: FdrAPI.Url("https://acme.docs.buildwithfern.com"), + }) + ); + const apiDefinition = + docs.definition.apis[apiDefinitionResponse.apiDefinitionId]; + expect(apiDefinition).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk + ).not.toEqual(undefined); + expect( + apiDefinition?.rootPackage.endpoints[0]?.examples[0]?.codeExamples.goSdk + ?.client + ).toEqual( + "client := userclient.New(userclient.WithAuthToken('YOUR_AUTH_TOKEN')" + ); }); diff --git a/servers/fdr/src/__test__/local/setupMockFdr.ts b/servers/fdr/src/__test__/local/setupMockFdr.ts index e09466c3b0..59e9dddb94 100644 --- a/servers/fdr/src/__test__/local/setupMockFdr.ts +++ b/servers/fdr/src/__test__/local/setupMockFdr.ts @@ -27,110 +27,118 @@ import { createMockFdrApplication } from "../mock"; let teardown = false; declare module "vitest" { - export interface ProvidedContext { - url: string; - } + export interface ProvidedContext { + url: string; + } } -export async function setup({ provide }: { provide: (key: string, value: any) => void }) { - await execa("docker-compose", ["-f", "docker-compose.test.yml", "up", "-d"], { stdio: "inherit" }); - await sleep(3000); - await execa("pnpm", ["prisma", "migrate", "deploy"], { - stdio: "inherit", +export async function setup({ + provide, +}: { + provide: (key: string, value: any) => void; +}) { + await execa("docker-compose", ["-f", "docker-compose.test.yml", "up", "-d"], { + stdio: "inherit", + }); + await sleep(3000); + await execa("pnpm", ["prisma", "migrate", "deploy"], { + stdio: "inherit", + }); + const instance = await runMockFdr(9999); + provide("url", `http://localhost:${instance.port}/`); + return async () => { + if (teardown) { + throw new Error("teardown called twice"); + } + teardown = true; + await execa("docker-compose", ["-f", "docker-compose.test.yml", "down"], { + stdio: "inherit", }); - const instance = await runMockFdr(9999); - provide("url", `http://localhost:${instance.port}/`); - return async () => { - if (teardown) { - throw new Error("teardown called twice"); - } - teardown = true; - await execa("docker-compose", ["-f", "docker-compose.test.yml", "down"], { stdio: "inherit" }); - return new Promise((resolve) => { - instance.server?.close(() => resolve()); - }); - }; + return new Promise((resolve) => { + instance.server?.close(() => resolve()); + }); + }; } export const prisma = new PrismaClient({ - log: ["query", "info", "warn", "error"], - transactionOptions: { - timeout: 15000, - maxWait: 15000, - }, + log: ["query", "info", "warn", "error"], + transactionOptions: { + timeout: 15000, + maxWait: 15000, + }, }); function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } declare namespace MockFdr { - interface Instance { - authedClient: FdrClient; - unauthedClient: FdrClient; - prisma: PrismaClient; - app: FdrApplication; - server: http.Server | undefined; - port: number; - } + interface Instance { + authedClient: FdrClient; + unauthedClient: FdrClient; + prisma: PrismaClient; + app: FdrApplication; + server: http.Server | undefined; + port: number; + } } async function runMockFdr(port: number): Promise { - const unauthedClient = new FdrClient({ - environment: `http://localhost:${port}/`, - }); - const authedClient = new FdrClient({ - environment: `http://localhost:${port}/`, - token: "dummy", - }); - const overrides: Partial = { redisEnabled: true }; - const fdrApplication = createMockFdrApplication({ - orgIds: ["acme", "octoai"], - configOverrides: overrides, - }); - const app = express(); - await fdrApplication.initialize(); - register(app, { - docs: { - v1: { - read: { _root: getDocsReadService(fdrApplication) }, - write: { _root: getDocsWriteService(fdrApplication) }, - }, - v2: { - read: { _root: getDocsReadV2Service(fdrApplication) }, - write: { _root: getDocsWriteV2Service(fdrApplication) }, - }, - }, - api: { - v1: { - read: { _root: getReadApiService(fdrApplication) }, - register: { _root: getRegisterApiService(fdrApplication) }, - }, - }, - snippets: getSnippetsService(fdrApplication), - snippetsFactory: getSnippetsFactoryService(fdrApplication), - templates: getTemplatesService(fdrApplication), - diff: getApiDiffService(fdrApplication), - docsCache: getDocsCacheService(fdrApplication), - sdks: { - versions: getVersionsService(fdrApplication), - }, - generators: { - _root: getGeneratorsRootController(fdrApplication), - cli: getGeneratorsCliController(fdrApplication), - versions: getGeneratorsVersionsController(fdrApplication), - }, - tokens: getTokensService(fdrApplication), - git: getGitController(fdrApplication), - }); - const server = app.listen(port); - console.log(`Mock FDR server running on http://localhost:${port}/`); - return { - authedClient, - unauthedClient, - prisma, - app: fdrApplication, - server, - port, - }; + const unauthedClient = new FdrClient({ + environment: `http://localhost:${port}/`, + }); + const authedClient = new FdrClient({ + environment: `http://localhost:${port}/`, + token: "dummy", + }); + const overrides: Partial = { redisEnabled: true }; + const fdrApplication = createMockFdrApplication({ + orgIds: ["acme", "octoai"], + configOverrides: overrides, + }); + const app = express(); + await fdrApplication.initialize(); + register(app, { + docs: { + v1: { + read: { _root: getDocsReadService(fdrApplication) }, + write: { _root: getDocsWriteService(fdrApplication) }, + }, + v2: { + read: { _root: getDocsReadV2Service(fdrApplication) }, + write: { _root: getDocsWriteV2Service(fdrApplication) }, + }, + }, + api: { + v1: { + read: { _root: getReadApiService(fdrApplication) }, + register: { _root: getRegisterApiService(fdrApplication) }, + }, + }, + snippets: getSnippetsService(fdrApplication), + snippetsFactory: getSnippetsFactoryService(fdrApplication), + templates: getTemplatesService(fdrApplication), + diff: getApiDiffService(fdrApplication), + docsCache: getDocsCacheService(fdrApplication), + sdks: { + versions: getVersionsService(fdrApplication), + }, + generators: { + _root: getGeneratorsRootController(fdrApplication), + cli: getGeneratorsCliController(fdrApplication), + versions: getGeneratorsVersionsController(fdrApplication), + }, + tokens: getTokensService(fdrApplication), + git: getGitController(fdrApplication), + }); + const server = app.listen(port); + console.log(`Mock FDR server running on http://localhost:${port}/`); + return { + authedClient, + unauthedClient, + prisma, + app: fdrApplication, + server, + port, + }; } diff --git a/servers/fdr/src/__test__/local/util.ts b/servers/fdr/src/__test__/local/util.ts index 4fda531b02..c5904ed6f9 100644 --- a/servers/fdr/src/__test__/local/util.ts +++ b/servers/fdr/src/__test__/local/util.ts @@ -2,115 +2,125 @@ import { APIResponse, APIV1Write, FdrClient } from "@fern-api/fdr-sdk"; import type { DocsV2, IndexSegment } from "@prisma/client"; export function getUniqueDocsForUrl(prefix: string): string { - return `${prefix}_${Math.random()}.fern.com`; + return `${prefix}_${Math.random()}.fern.com`; } export function createApiDefinition({ - endpointId, - endpointPath, - endpointMethod, - snippetsConfig, - originalEndpointId, + endpointId, + endpointPath, + endpointMethod, + snippetsConfig, + originalEndpointId, }: { - endpointId: APIV1Write.EndpointId; - endpointPath: APIV1Write.EndpointPath; - endpointMethod: APIV1Write.HttpMethod; - snippetsConfig?: APIV1Write.SnippetsConfig; - originalEndpointId?: string; + endpointId: APIV1Write.EndpointId; + endpointPath: APIV1Write.EndpointPath; + endpointMethod: APIV1Write.HttpMethod; + snippetsConfig?: APIV1Write.SnippetsConfig; + originalEndpointId?: string; }): APIV1Write.ApiDefinition { - return { - rootPackage: { - endpoints: [ - { - id: endpointId, - originalEndpointId, - method: endpointMethod, - path: endpointPath, - headers: [], - queryParameters: [], - examples: [], - auth: undefined, - defaultEnvironment: undefined, - environments: undefined, - name: undefined, - request: undefined, - response: undefined, - errors: undefined, - errorsV2: undefined, - description: undefined, - availability: undefined, - }, - ], - types: [], - subpackages: [], - websockets: [], - webhooks: undefined, - pointsTo: undefined, + return { + rootPackage: { + endpoints: [ + { + id: endpointId, + originalEndpointId, + method: endpointMethod, + path: endpointPath, + headers: [], + queryParameters: [], + examples: [], + auth: undefined, + defaultEnvironment: undefined, + environments: undefined, + name: undefined, + request: undefined, + response: undefined, + errors: undefined, + errorsV2: undefined, + description: undefined, + availability: undefined, }, - subpackages: {}, - types: {}, - snippetsConfiguration: snippetsConfig, - auth: undefined, - globalHeaders: undefined, - navigation: undefined, - }; + ], + types: [], + subpackages: [], + websockets: [], + webhooks: undefined, + pointsTo: undefined, + }, + subpackages: {}, + types: {}, + snippetsConfiguration: snippetsConfig, + auth: undefined, + globalHeaders: undefined, + navigation: undefined, + }; } export function createMockDocs({ + domain, + path, + indexSegmentIds, +}: { + domain: string; + path: string; + indexSegmentIds: string[]; +}): DocsV2 { + return { domain, path, indexSegmentIds, -}: { - domain: string; - path: string; - indexSegmentIds: string[]; -}): DocsV2 { - return { - domain, - path, - indexSegmentIds, - algoliaIndex: null, - docsDefinition: Buffer.from("nil"), - orgID: "", - updatedTime: new Date(), - docsConfigInstanceId: "123", - isPreview: false, - authType: "PUBLIC", - hasPublicS3Assets: true, - }; + algoliaIndex: null, + docsDefinition: Buffer.from("nil"), + orgID: "", + updatedTime: new Date(), + docsConfigInstanceId: "123", + isPreview: false, + authType: "PUBLIC", + hasPublicS3Assets: true, + }; } export function createMockIndexSegment({ - id, - version, - createdAt, + id, + version, + createdAt, }: { - id: string; - version?: string | null; - createdAt: Date; + id: string; + version?: string | null; + createdAt: Date; }): IndexSegment { - return { - id, - version: version ?? null, - createdAt, - }; + return { + id, + version: version ?? null, + createdAt, + }; } -export function getAPIResponse(response: APIResponse): Success { - if (response.ok) { - return response.body; - } - throw new Error(`Received error from response: ${JSON.stringify(response.error)}`); +export function getAPIResponse( + response: APIResponse +): Success { + if (response.ok) { + return response.body; + } + throw new Error( + `Received error from response: ${JSON.stringify(response.error)}` + ); } -export function getClient({ authed, url }: { url: string; authed: boolean }): FdrClient { - if (authed) { - return new FdrClient({ - environment: url, - token: "dummy", - }); - } +export function getClient({ + authed, + url, +}: { + url: string; + authed: boolean; +}): FdrClient { + if (authed) { return new FdrClient({ - environment: url, + environment: url, + token: "dummy", }); + } + return new FdrClient({ + environment: url, + }); } diff --git a/servers/fdr/src/__test__/local/vitest.config.ts b/servers/fdr/src/__test__/local/vitest.config.ts index cca3de5ef8..863e6276b7 100644 --- a/servers/fdr/src/__test__/local/vitest.config.ts +++ b/servers/fdr/src/__test__/local/vitest.config.ts @@ -1,10 +1,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - globalSetup: ["src/__test__/local/setupMockFdr.ts"], - // Don't run in parallel so it's easy to manage db state across tests. - fileParallelism: false, - }, + test: { + globals: true, + globalSetup: ["src/__test__/local/setupMockFdr.ts"], + // Don't run in parallel so it's easy to manage db state across tests. + fileParallelism: false, + }, }); diff --git a/servers/fdr/src/__test__/mock.ts b/servers/fdr/src/__test__/mock.ts index 3d0ceeb09b..b56f61140f 100644 --- a/servers/fdr/src/__test__/mock.ts +++ b/servers/fdr/src/__test__/mock.ts @@ -1,181 +1,202 @@ import { APIV1Db, DocsV1Db } from "@fern-api/fdr-sdk"; import { FdrApplication, type FdrConfig } from "../app"; import { type FdrServices } from "../app/FdrApplication"; -import { ConfigSegmentTuple, type AlgoliaSearchRecord, type AlgoliaService } from "../services/algolia"; +import { + ConfigSegmentTuple, + type AlgoliaSearchRecord, + type AlgoliaService, +} from "../services/algolia"; import { type AuthService } from "../services/auth"; import { OrgIdsResponse } from "../services/auth/AuthService"; -import { RevalidatedPathsResponse, RevalidatorService } from "../services/revalidator/RevalidatorService"; import { - FailedToDeleteIndexSegment, - FailedToRegisterDocsNotification, - FailedToRevalidatePathsNotification, - GeneratingDocsNotification, - SlackService, + RevalidatedPathsResponse, + RevalidatorService, +} from "../services/revalidator/RevalidatorService"; +import { + FailedToDeleteIndexSegment, + FailedToRegisterDocsNotification, + FailedToRevalidatePathsNotification, + GeneratingDocsNotification, + SlackService, } from "../services/slack/SlackService"; import { ParsedBaseUrl } from "../util/ParsedBaseUrl"; export class MockAlgoliaService implements AlgoliaService { - generateSearchApiKey(_filters: string): string { - return ""; - } - - async deleteIndexSegmentRecords(_indexSegmentIds: string[]): Promise { - return; - } - - async generateSearchRecords(_: { - docsDefinition: DocsV1Db.DocsDefinitionDb; - apiDefinitionsById: Record; - configSegmentTuples: ConfigSegmentTuple[]; - }): Promise { - return []; - } - - async uploadSearchRecords(_records: AlgoliaSearchRecord[]): Promise { - return; - } + generateSearchApiKey(_filters: string): string { + return ""; + } + + async deleteIndexSegmentRecords(_indexSegmentIds: string[]): Promise { + return; + } + + async generateSearchRecords(_: { + docsDefinition: DocsV1Db.DocsDefinitionDb; + apiDefinitionsById: Record; + configSegmentTuples: ConfigSegmentTuple[]; + }): Promise { + return []; + } + + async uploadSearchRecords(_records: AlgoliaSearchRecord[]): Promise { + return; + } } class MockAuthService implements AuthService { - orgIds: string[]; - - constructor({ orgIds }: { orgIds: string[] }) { - this.orgIds = orgIds; - } - - async checkUserBelongsToOrg(): Promise { - return; - } - - async getOrgIdsFromAuthHeader(_authHeader: { authHeader: string | undefined }): Promise { - return { - type: "success", - orgIds: new Set(this.orgIds), - }; - } - - checkOrgHasSnippetsApiAccess({ - authHeader, - orgId, - failHard, - }: { - authHeader: string | undefined; - orgId: string; - failHard?: boolean | undefined; - }): Promise { - return Promise.resolve(false); - } - - checkOrgHasSnippetTemplateAccess({ - authHeader, - orgId, - failHard, - }: { - authHeader: string | undefined; - orgId: string; - failHard?: boolean | undefined; - }): Promise { - return Promise.resolve(false); - } - - async getWorkOSOrganization(_orgId: { orgId: string }): Promise { - return undefined; - } + orgIds: string[]; + + constructor({ orgIds }: { orgIds: string[] }) { + this.orgIds = orgIds; + } + + async checkUserBelongsToOrg(): Promise { + return; + } + + async getOrgIdsFromAuthHeader(_authHeader: { + authHeader: string | undefined; + }): Promise { + return { + type: "success", + orgIds: new Set(this.orgIds), + }; + } + + checkOrgHasSnippetsApiAccess({ + authHeader, + orgId, + failHard, + }: { + authHeader: string | undefined; + orgId: string; + failHard?: boolean | undefined; + }): Promise { + return Promise.resolve(false); + } + + checkOrgHasSnippetTemplateAccess({ + authHeader, + orgId, + failHard, + }: { + authHeader: string | undefined; + orgId: string; + failHard?: boolean | undefined; + }): Promise { + return Promise.resolve(false); + } + + async getWorkOSOrganization(_orgId: { + orgId: string; + }): Promise { + return undefined; + } } class MockSlackService implements SlackService { - async notify(_message: string, _err: unknown): Promise { - return; - } - - async notifyFailedToRegisterDocs(_request: FailedToRegisterDocsNotification): Promise { - return; - } - - async notifyFailedToRevalidatePaths(_request: FailedToRevalidatePathsNotification): Promise { - return; - } - - async notifyFailedToDeleteIndexSegment(_request: FailedToDeleteIndexSegment): Promise { - return; - } - - async notifyGeneratedDocs(_request: GeneratingDocsNotification): Promise { - return; - } + async notify(_message: string, _err: unknown): Promise { + return; + } + + async notifyFailedToRegisterDocs( + _request: FailedToRegisterDocsNotification + ): Promise { + return; + } + + async notifyFailedToRevalidatePaths( + _request: FailedToRevalidatePathsNotification + ): Promise { + return; + } + + async notifyFailedToDeleteIndexSegment( + _request: FailedToDeleteIndexSegment + ): Promise { + return; + } + + async notifyGeneratedDocs( + _request: GeneratingDocsNotification + ): Promise { + return; + } } class MockRevalidatorService implements RevalidatorService { - async revalidate(_params: { baseUrl: ParsedBaseUrl }): Promise { - return { - successful: [], - failed: [], - revalidationFailed: false, - }; - } + async revalidate(_params: { + baseUrl: ParsedBaseUrl; + }): Promise { + return { + successful: [], + failed: [], + revalidationFailed: false, + }; + } } export const baseMockFdrConfig: FdrConfig = { - awsAccessKey: "", - awsSecretKey: "", - cdnPublicDocsUrl: "https://files.buildwithfern.com", - publicDocsS3: { - bucketName: "fdr", - bucketRegion: "us-east-1", - urlOverride: "http://s3-mock:9090", - }, - privateDocsS3: { - bucketName: "fdr", - bucketRegion: "us-east-1", - urlOverride: "http://s3-mock:9090", - }, - privateApiDefinitionSourceS3: { - bucketName: "fdr", - bucketRegion: "us-east-1", - urlOverride: "http://s3-mock:9090", - }, - venusUrl: "", - domainSuffix: ".docs.buildwithfern.com", - algoliaAppId: "", - algoliaAdminApiKey: "", - algoliaSearchIndex: "", - algoliaSearchApiKey: "", - algoliaSearchV2Domains: [], - slackToken: "", - logLevel: "debug", - docsCacheEndpoint: process.env.DOCS_CACHE_ENDPOINT || "", - enableCustomerNotifications: false, - applicationEnvironment: "mock", - redisEnabled: false, - redisClusteringEnabled: false, + awsAccessKey: "", + awsSecretKey: "", + cdnPublicDocsUrl: "https://files.buildwithfern.com", + publicDocsS3: { + bucketName: "fdr", + bucketRegion: "us-east-1", + urlOverride: "http://s3-mock:9090", + }, + privateDocsS3: { + bucketName: "fdr", + bucketRegion: "us-east-1", + urlOverride: "http://s3-mock:9090", + }, + privateApiDefinitionSourceS3: { + bucketName: "fdr", + bucketRegion: "us-east-1", + urlOverride: "http://s3-mock:9090", + }, + venusUrl: "", + domainSuffix: ".docs.buildwithfern.com", + algoliaAppId: "", + algoliaAdminApiKey: "", + algoliaSearchIndex: "", + algoliaSearchApiKey: "", + algoliaSearchV2Domains: [], + slackToken: "", + logLevel: "debug", + docsCacheEndpoint: process.env.DOCS_CACHE_ENDPOINT || "", + enableCustomerNotifications: false, + applicationEnvironment: "mock", + redisEnabled: false, + redisClusteringEnabled: false, }; export function getMockFdrConfig(overrides?: Partial): FdrConfig { - if (overrides) { - return { - ...baseMockFdrConfig, - ...overrides, - }; - } - return baseMockFdrConfig; + if (overrides) { + return { + ...baseMockFdrConfig, + ...overrides, + }; + } + return baseMockFdrConfig; } export function createMockFdrApplication({ - orgIds, - services, - configOverrides, + orgIds, + services, + configOverrides, }: { - orgIds?: string[]; - services?: Partial; - configOverrides?: Partial; + orgIds?: string[]; + services?: Partial; + configOverrides?: Partial; }) { - return new FdrApplication(getMockFdrConfig(configOverrides), { - auth: new MockAuthService({ - orgIds: orgIds ?? [], - }), - algolia: new MockAlgoliaService(), - slack: new MockSlackService(), - revalidator: new MockRevalidatorService(), - ...services, - }); + return new FdrApplication(getMockFdrConfig(configOverrides), { + auth: new MockAuthService({ + orgIds: orgIds ?? [], + }), + algolia: new MockAlgoliaService(), + slack: new MockSlackService(), + revalidator: new MockRevalidatorService(), + ...services, + }); } diff --git a/servers/fdr/src/__test__/octo.ts b/servers/fdr/src/__test__/octo.ts index 9d9be8e4a3..bf4e064842 100644 --- a/servers/fdr/src/__test__/octo.ts +++ b/servers/fdr/src/__test__/octo.ts @@ -1,683 +1,692 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; -export const CHAT_COMPLETION_SNIPPET = (version: string): FdrAPI.EndpointSnippetTemplate => ({ - endpointId: { - path: FdrAPI.EndpointPathLiteral("/v1/chat/completions"), - method: "POST", - identifierOverride: undefined, - }, - sdk: { - type: "python", - package: "octoai", - version, - }, - snippetTemplate: { - type: "v1", - functionInvocation: { +export const CHAT_COMPLETION_SNIPPET = ( + version: string +): FdrAPI.EndpointSnippetTemplate => ({ + endpointId: { + path: FdrAPI.EndpointPathLiteral("/v1/chat/completions"), + method: "POST", + identifierOverride: undefined, + }, + sdk: { + type: "python", + package: "octoai", + version, + }, + snippetTemplate: { + type: "v1", + functionInvocation: { + imports: [], + isOptional: true, + templateString: + "await client.text_gen.create_chat_completion_stream(\n$FERN_INPUT\n)", + templateInputs: [ + { + type: "template", + value: { imports: [], isOptional: true, - templateString: "await client.text_gen.create_chat_completion_stream(\n$FERN_INPUT\n)", + templateString: "frequency_penalty=$FERN_INPUT", templateInputs: [ + { + location: "BODY", + path: "frequency_penalty", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + containerTemplateString: "functions=[$FERN_INPUT]", + delimiter: ", ", + innerTemplate: { + imports: ["from .function import Function"], + isOptional: true, + templateString: "Function($FERN_INPUT)", + templateInputs: [ { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "frequency_penalty=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "frequency_penalty", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "description=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "description", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, }, { - type: "template", - value: { - imports: [], - isOptional: true, - containerTemplateString: "functions=[$FERN_INPUT]", - delimiter: ", ", - innerTemplate: { - imports: ["from .function import Function"], - isOptional: true, - templateString: "Function($FERN_INPUT)", - templateInputs: [ - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "description=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "description", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "name=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "name", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "parameters=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "parameters", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - ], - inputDelimiter: ",\n", - type: "generic", - }, - templateInput: { - location: "BODY", - path: "functions", - }, - type: "iterable", - }, + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "name=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "name", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, }, { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "ignore_eos=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "ignore_eos", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "parameters=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "parameters", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, }, + ], + inputDelimiter: ",\n", + type: "generic", + }, + templateInput: { + location: "BODY", + path: "functions", + }, + type: "iterable", + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "ignore_eos=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "ignore_eos", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + containerTemplateString: "logit_bias={$FERN_INPUT}", + delimiter: ", ", + keyTemplate: { + imports: [], + isOptional: true, + templateString: "$FERN_INPUT", + templateInputs: [ { - type: "template", - value: { - imports: [], - isOptional: true, - containerTemplateString: "logit_bias={$FERN_INPUT}", - delimiter: ", ", - keyTemplate: { - imports: [], - isOptional: true, - templateString: "$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: undefined, - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - valueTemplate: { - imports: [], - isOptional: true, - templateString: "$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: undefined, - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - keyValueSeparator: ": ", - templateInput: { - location: "BODY", - path: "logit_bias", - }, - type: "dict", - }, + location: "BODY", + path: undefined, + type: "payload", }, + ], + type: "generic", + inputDelimiter: undefined, + }, + valueTemplate: { + imports: [], + isOptional: true, + templateString: "$FERN_INPUT", + templateInputs: [ { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "max_tokens=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "max_tokens", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, + location: "BODY", + path: undefined, + type: "payload", }, + ], + type: "generic", + inputDelimiter: undefined, + }, + keyValueSeparator: ": ", + templateInput: { + location: "BODY", + path: "logit_bias", + }, + type: "dict", + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "max_tokens=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "max_tokens", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + containerTemplateString: "messages=[$FERN_INPUT]", + delimiter: ", ", + innerTemplate: { + imports: ["from .chat_message import ChatMessage"], + isOptional: true, + templateString: "ChatMessage($FERN_INPUT)", + templateInputs: [ { - type: "template", - value: { - imports: [], - isOptional: true, - containerTemplateString: "messages=[$FERN_INPUT]", - delimiter: ", ", - innerTemplate: { - imports: ["from .chat_message import ChatMessage"], - isOptional: true, - templateString: "ChatMessage($FERN_INPUT)", - templateInputs: [ - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "content=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "content", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: ["from .chat_fn_call import ChatFnCall"], - isOptional: true, - templateString: "function_call=ChatFnCall(\n$FERN_INPUT\n)", - templateInputs: [ - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "arguments=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "function_call.arguments", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "name=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "function_call.name", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - ], - inputDelimiter: ",\n", - type: "generic", - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "role=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "role", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - ], - inputDelimiter: ",\n", - type: "generic", - }, - templateInput: { - location: "BODY", - path: "messages", - }, - type: "iterable", - }, + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "content=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "content", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, }, { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "model=$FERN_INPUT", - templateInputs: [ + type: "template", + value: { + imports: ["from .chat_fn_call import ChatFnCall"], + isOptional: true, + templateString: "function_call=ChatFnCall(\n$FERN_INPUT\n)", + templateInputs: [ + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "arguments=$FERN_INPUT", + templateInputs: [ { - location: "BODY", - path: "model", - type: "payload", + location: "BODY", + path: "function_call.arguments", + type: "payload", }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "n=$FERN_INPUT", - templateInputs: [ + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "name=$FERN_INPUT", + templateInputs: [ { - location: "BODY", - path: "n", - type: "payload", + location: "BODY", + path: "function_call.name", + type: "payload", }, - ], - type: "generic", - inputDelimiter: undefined, - }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + ], + inputDelimiter: ",\n", + type: "generic", + }, }, { - type: "template", - value: { - imports: ["from .chat_completion_request_ext import ChatCompletionRequestExt"], - isOptional: true, - templateString: "octoai=ChatCompletionRequestExt(\n$FERN_INPUT\n)", - templateInputs: [ - { - type: "template", - value: { - imports: [], - isOptional: true, - containerTemplateString: "loras=[$FERN_INPUT]", - delimiter: ", ", - innerTemplate: { - imports: [], - isOptional: true, - templateString: "$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: undefined, - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - templateInput: { - location: "BODY", - path: "octoai.loras", - }, - type: "iterable", - }, - }, - { - type: "template", - value: { - imports: [ - "from .chat_completion_request_ext_vllm import ChatCompletionRequestExtVllm", - ], - isOptional: true, - templateString: "vllm=ChatCompletionRequestExtVllm(\n$FERN_INPUT\n)", - templateInputs: [ - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "best_of=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "octoai.vllm.best_of", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "ignore_eos=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "octoai.vllm.best_of.vllm.ignore_eos", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "skip_special_tokens=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - containerTemplateString: "stop_token_ids=[$FERN_INPUT]", - delimiter: ", ", - innerTemplate: { - imports: [], - isOptional: true, - templateString: "$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: undefined, - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - templateInput: { - location: "BODY", - path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens.vllm.stop_token_ids", - }, - type: "iterable", - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "top_k=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens.vllm.stop_token_ids.vllm.top_k", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "use_beam_search=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens.vllm.stop_token_ids.vllm.top_k.vllm.use_beam_search", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, - ], - inputDelimiter: ",\n", - type: "generic", - }, - }, - ], - inputDelimiter: ",\n", - type: "generic", - }, + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "role=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "role", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, }, - { - type: "template", - value: { + ], + inputDelimiter: ",\n", + type: "generic", + }, + templateInput: { + location: "BODY", + path: "messages", + }, + type: "iterable", + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "model=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "model", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "n=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "n", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [ + "from .chat_completion_request_ext import ChatCompletionRequestExt", + ], + isOptional: true, + templateString: "octoai=ChatCompletionRequestExt(\n$FERN_INPUT\n)", + templateInputs: [ + { + type: "template", + value: { + imports: [], + isOptional: true, + containerTemplateString: "loras=[$FERN_INPUT]", + delimiter: ", ", + innerTemplate: { + imports: [], + isOptional: true, + templateString: "$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: undefined, + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + templateInput: { + location: "BODY", + path: "octoai.loras", + }, + type: "iterable", + }, + }, + { + type: "template", + value: { + imports: [ + "from .chat_completion_request_ext_vllm import ChatCompletionRequestExtVllm", + ], + isOptional: true, + templateString: + "vllm=ChatCompletionRequestExtVllm(\n$FERN_INPUT\n)", + templateInputs: [ + { + type: "template", + value: { imports: [], isOptional: true, - templateString: "presence_penalty=$FERN_INPUT", + templateString: "best_of=$FERN_INPUT", templateInputs: [ - { - location: "BODY", - path: "presence_penalty", - type: "payload", - }, + { + location: "BODY", + path: "octoai.vllm.best_of", + type: "payload", + }, ], type: "generic", inputDelimiter: undefined, + }, }, - }, - { - type: "template", - value: { + { + type: "template", + value: { imports: [], isOptional: true, - templateString: "repetition_penalty=$FERN_INPUT", + templateString: "ignore_eos=$FERN_INPUT", templateInputs: [ - { - location: "BODY", - path: "repetition_penalty", - type: "payload", - }, + { + location: "BODY", + path: "octoai.vllm.best_of.vllm.ignore_eos", + type: "payload", + }, ], type: "generic", inputDelimiter: undefined, + }, }, - }, - { - type: "template", - value: { - imports: ["from .chat_completion_response_format import ChatCompletionResponseFormat"], + { + type: "template", + value: { + imports: [], isOptional: true, - templateString: "response_format=ChatCompletionResponseFormat(\n$FERN_INPUT\n)", + templateString: "skip_special_tokens=$FERN_INPUT", templateInputs: [ - { - type: "template", - value: { - imports: [], - isOptional: true, - containerTemplateString: "schema={$FERN_INPUT}", - delimiter: ", ", - keyTemplate: { - imports: [], - isOptional: true, - templateString: "$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: undefined, - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - valueTemplate: { - imports: [], - isOptional: true, - templateString: "$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: undefined, - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - keyValueSeparator: ": ", - templateInput: { - location: "BODY", - path: "response_format.schema", - }, - type: "dict", - }, - }, - { - type: "template", - value: { - imports: [], - isOptional: true, - templateString: "type=$FERN_INPUT", - templateInputs: [ - { - location: "BODY", - path: "response_format.type", - type: "payload", - }, - ], - type: "generic", - inputDelimiter: undefined, - }, - }, + { + location: "BODY", + path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens", + type: "payload", + }, ], - inputDelimiter: ",\n", type: "generic", + inputDelimiter: undefined, + }, }, - }, - { - type: "template", - value: { + { + type: "template", + value: { imports: [], isOptional: true, - templateString: "temperature=$FERN_INPUT", - templateInputs: [ + containerTemplateString: "stop_token_ids=[$FERN_INPUT]", + delimiter: ", ", + innerTemplate: { + imports: [], + isOptional: true, + templateString: "$FERN_INPUT", + templateInputs: [ { - location: "BODY", - path: "temperature", - type: "payload", + location: "BODY", + path: undefined, + type: "payload", }, - ], - type: "generic", - inputDelimiter: undefined, + ], + type: "generic", + inputDelimiter: undefined, + }, + templateInput: { + location: "BODY", + path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens.vllm.stop_token_ids", + }, + type: "iterable", + }, }, - }, - { - type: "template", - value: { + { + type: "template", + value: { imports: [], isOptional: true, - templateString: "top_p=$FERN_INPUT", + templateString: "top_k=$FERN_INPUT", templateInputs: [ - { - location: "BODY", - path: "top_p", - type: "payload", - }, + { + location: "BODY", + path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens.vllm.stop_token_ids.vllm.top_k", + type: "payload", + }, ], type: "generic", inputDelimiter: undefined, + }, }, - }, - { - type: "template", - value: { + { + type: "template", + value: { imports: [], isOptional: true, - templateString: "user=$FERN_INPUT", + templateString: "use_beam_search=$FERN_INPUT", templateInputs: [ - { - location: "BODY", - path: "user", - type: "payload", - }, + { + location: "BODY", + path: "octoai.vllm.best_of.vllm.ignore_eos.vllm.skip_special_tokens.vllm.stop_token_ids.vllm.top_k.vllm.use_beam_search", + type: "payload", + }, ], type: "generic", inputDelimiter: undefined, + }, + }, + ], + inputDelimiter: ",\n", + type: "generic", + }, + }, + ], + inputDelimiter: ",\n", + type: "generic", + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "presence_penalty=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "presence_penalty", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "repetition_penalty=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "repetition_penalty", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [ + "from .chat_completion_response_format import ChatCompletionResponseFormat", + ], + isOptional: true, + templateString: + "response_format=ChatCompletionResponseFormat(\n$FERN_INPUT\n)", + templateInputs: [ + { + type: "template", + value: { + imports: [], + isOptional: true, + containerTemplateString: "schema={$FERN_INPUT}", + delimiter: ", ", + keyTemplate: { + imports: [], + isOptional: true, + templateString: "$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: undefined, + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + valueTemplate: { + imports: [], + isOptional: true, + templateString: "$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: undefined, + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + keyValueSeparator: ": ", + templateInput: { + location: "BODY", + path: "response_format.schema", + }, + type: "dict", + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "type=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "response_format.type", + type: "payload", }, + ], + type: "generic", + inputDelimiter: undefined, }, + }, ], inputDelimiter: ",\n", type: "generic", + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "temperature=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "temperature", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, }, - clientInstantiation: - 'from octoai.client import AsyncOctoAI\n\nclient = AsyncOctoAI(\n api_key="YOUR_API_KEY",\n)\n', + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "top_p=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "top_p", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + { + type: "template", + value: { + imports: [], + isOptional: true, + templateString: "user=$FERN_INPUT", + templateInputs: [ + { + location: "BODY", + path: "user", + type: "payload", + }, + ], + type: "generic", + inputDelimiter: undefined, + }, + }, + ], + inputDelimiter: ",\n", + type: "generic", }, - apiDefinitionId: undefined, - additionalTemplates: undefined, + clientInstantiation: + 'from octoai.client import AsyncOctoAI\n\nclient = AsyncOctoAI(\n api_key="YOUR_API_KEY",\n)\n', + }, + apiDefinitionId: undefined, + additionalTemplates: undefined, }); export const CHAT_COMPLETION_PAYLOAD: FdrAPI.CustomSnippetPayload = { - headers: [], - pathParameters: [], - queryParameters: [], - requestBody: { - messages: [ - { - role: "system", - content: "You are a helpful assistant.", - }, - { - role: "user", - content: "Hello world", - }, - ], - model: "qwen1.5-32b-chat", - max_tokens: 512, - presence_penalty: 0, - temperature: 0.1, - top_p: 0.9, - }, - auth: undefined, + headers: [], + pathParameters: [], + queryParameters: [], + requestBody: { + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "Hello world", + }, + ], + model: "qwen1.5-32b-chat", + max_tokens: 512, + presence_penalty: 0, + temperature: 0.1, + top_p: 0.9, + }, + auth: undefined, }; diff --git a/servers/fdr/src/__test__/unit-tests/Cache.test.ts b/servers/fdr/src/__test__/unit-tests/Cache.test.ts index 2421ac8e9b..e3ebb205d3 100644 --- a/servers/fdr/src/__test__/unit-tests/Cache.test.ts +++ b/servers/fdr/src/__test__/unit-tests/Cache.test.ts @@ -1,46 +1,46 @@ import { Cache } from "../../Cache"; describe("Cache", () => { - it("should be able to set and get values", () => { - const cache = new Cache(10); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - expect(cache.get("key1")).toBe("value1"); - expect(cache.get("key2")).toBe("value2"); - cache.set("key1", "value3"); - expect(cache.get("key1")).toBe("value3"); - expect(cache.get("key2")).toBe("value2"); - }); + it("should be able to set and get values", () => { + const cache = new Cache(10); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + expect(cache.get("key1")).toBe("value1"); + expect(cache.get("key2")).toBe("value2"); + cache.set("key1", "value3"); + expect(cache.get("key1")).toBe("value3"); + expect(cache.get("key2")).toBe("value2"); + }); - it("should be able to delete the oldest keys", () => { - const cache = new Cache(2); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - cache.set("key3", "value3"); - expect(cache.get("key1")).toBe(undefined); - expect(cache.get("key2")).toBe("value2"); - expect(cache.get("key3")).toBe("value3"); - }); + it("should be able to delete the oldest keys", () => { + const cache = new Cache(2); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + cache.set("key3", "value3"); + expect(cache.get("key1")).toBe(undefined); + expect(cache.get("key2")).toBe("value2"); + expect(cache.get("key3")).toBe("value3"); + }); - it("should be able to delete keys based on ttl", () => { - vitest.useFakeTimers(); - const cache = new Cache(10, 1); - cache.set("key1", "value1"); - vitest.advanceTimersByTime(1000); - cache.set("key2", "value2"); - expect(cache.get("key1")).toBe(undefined); - expect(cache.get("key2")).toBe("value2"); - }); + it("should be able to delete keys based on ttl", () => { + vitest.useFakeTimers(); + const cache = new Cache(10, 1); + cache.set("key1", "value1"); + vitest.advanceTimersByTime(1000); + cache.set("key2", "value2"); + expect(cache.get("key1")).toBe(undefined); + expect(cache.get("key2")).toBe("value2"); + }); - it("should be able to delete keys based on ttl and max keys", () => { - vitest.useFakeTimers(); - const cache = new Cache(2, 1); - cache.set("key1", "value1"); - cache.set("key2", "value2"); - vitest.advanceTimersByTime(1000); - cache.set("key3", "value3"); - expect(cache.get("key1")).toBe(undefined); - expect(cache.get("key2")).toBe(undefined); - expect(cache.get("key3")).toBe("value3"); - }); + it("should be able to delete keys based on ttl and max keys", () => { + vitest.useFakeTimers(); + const cache = new Cache(2, 1); + cache.set("key1", "value1"); + cache.set("key2", "value2"); + vitest.advanceTimersByTime(1000); + cache.set("key3", "value3"); + expect(cache.get("key1")).toBe(undefined); + expect(cache.get("key2")).toBe(undefined); + expect(cache.get("key3")).toBe("value3"); + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/ParsedBaseUrl.test.ts b/servers/fdr/src/__test__/unit-tests/ParsedBaseUrl.test.ts index 205b9c4a2b..bbef9e18e0 100644 --- a/servers/fdr/src/__test__/unit-tests/ParsedBaseUrl.test.ts +++ b/servers/fdr/src/__test__/unit-tests/ParsedBaseUrl.test.ts @@ -1,31 +1,35 @@ import { ParsedBaseUrl } from "../../util/ParsedBaseUrl"; describe("ParsedBaseUrl", () => { - it("fern.docs.buildwithfern.com", () => { - const parsedUrl = ParsedBaseUrl.parse("https://fern.docs.buildwithfern.com"); - expect(parsedUrl.hostname).toEqual("fern.docs.buildwithfern.com"); - expect(parsedUrl.path).toBeUndefined(); - expect(parsedUrl.getFullUrl()).toEqual("fern.docs.buildwithfern.com"); - }); + it("fern.docs.buildwithfern.com", () => { + const parsedUrl = ParsedBaseUrl.parse( + "https://fern.docs.buildwithfern.com" + ); + expect(parsedUrl.hostname).toEqual("fern.docs.buildwithfern.com"); + expect(parsedUrl.path).toBeUndefined(); + expect(parsedUrl.getFullUrl()).toEqual("fern.docs.buildwithfern.com"); + }); - it("buildwithfern.com/docs", () => { - const parsedUrl = ParsedBaseUrl.parse("https://buildwithfern.com/docs"); - expect(parsedUrl.hostname).toEqual("buildwithfern.com"); - expect(parsedUrl.path).toEqual("/docs"); - expect(parsedUrl.getFullUrl()).toEqual("buildwithfern.com/docs"); - }); + it("buildwithfern.com/docs", () => { + const parsedUrl = ParsedBaseUrl.parse("https://buildwithfern.com/docs"); + expect(parsedUrl.hostname).toEqual("buildwithfern.com"); + expect(parsedUrl.path).toEqual("/docs"); + expect(parsedUrl.getFullUrl()).toEqual("buildwithfern.com/docs"); + }); - it("apidocs.polytomic.com", () => { - const parsedUrl = ParsedBaseUrl.parse("apidocs.polytomic.com"); - expect(parsedUrl.hostname).toEqual("apidocs.polytomic.com"); - expect(parsedUrl.path).toEqual(undefined); - expect(parsedUrl.toURL()).toEqual(new URL("https://apidocs.polytomic.com")); - }); + it("apidocs.polytomic.com", () => { + const parsedUrl = ParsedBaseUrl.parse("apidocs.polytomic.com"); + expect(parsedUrl.hostname).toEqual("apidocs.polytomic.com"); + expect(parsedUrl.path).toEqual(undefined); + expect(parsedUrl.toURL()).toEqual(new URL("https://apidocs.polytomic.com")); + }); - it("polytomic.docs.buildwithfern.com", () => { - const parsedUrl = ParsedBaseUrl.parse("polytomic.docs.buildwithfern.com"); - expect(parsedUrl.hostname).toEqual("polytomic.docs.buildwithfern.com"); - expect(parsedUrl.path).toEqual(undefined); - expect(parsedUrl.toURL()).toEqual(new URL("https://polytomic.docs.buildwithfern.com")); - }); + it("polytomic.docs.buildwithfern.com", () => { + const parsedUrl = ParsedBaseUrl.parse("polytomic.docs.buildwithfern.com"); + expect(parsedUrl.hostname).toEqual("polytomic.docs.buildwithfern.com"); + expect(parsedUrl.path).toEqual(undefined); + expect(parsedUrl.toURL()).toEqual( + new URL("https://polytomic.docs.buildwithfern.com") + ); + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/algolia.test.ts b/servers/fdr/src/__test__/unit-tests/algolia.test.ts index 32626686da..619740c896 100644 --- a/servers/fdr/src/__test__/unit-tests/algolia.test.ts +++ b/servers/fdr/src/__test__/unit-tests/algolia.test.ts @@ -1,12 +1,15 @@ import { FdrAPI, FernNavigation } from "@fern-api/fdr-sdk"; -import { getMarkdownSectionTree, getMarkdownSections } from "../../services/algolia/AlgoliaSearchRecordGeneratorV2"; +import { + getMarkdownSectionTree, + getMarkdownSections, +} from "../../services/algolia/AlgoliaSearchRecordGeneratorV2"; describe("algolia utils", () => { - it("should extract headers into a tree from markdown content 1", () => { - expect( - getMarkdownSections( - getMarkdownSectionTree( - `--- + it("should extract headers into a tree from markdown content 1", () => { + expect( + getMarkdownSections( + getMarkdownSectionTree( + `--- title: Overview subtitle: A comprehensive reference for integrating with Chariot API endpoints --- @@ -31,78 +34,78 @@ https://api.givechariot.com (Production) Chariot has two environments: Sandbox and Production. The Sandbox environment supports only test data. All activity in the Production environment is real. When you’re getting ready to launch into production, please let us know by emailing [support@givechariot.com](support@givechariot.com) to get your production credentials. `, - "Overview", - ), - [], - FdrAPI.IndexSegmentId("testindex"), - FernNavigation.V1.Slug("v1/someslug"), - ).map((record) => { - return { - ...record, - objectID: undefined, - }; - }), - ).toEqual([ - { - breadcrumbs: [ - { - slug: "v1/someslug", - title: "Overview", - }, - ], - content: - "The Chariot API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n\nIf you feel like something is missing from our API docs, feel free to create an Issue on our [OpenAPI GitHub repo](https://github.com/chariot-giving/chariot-openapi).\n", - indexSegmentId: "testindex", - objectID: undefined, - slug: "v1/someslug", - title: "Overview", - type: "markdown-section-v1", - }, - { - breadcrumbs: [ - { - slug: "v1/someslug", - title: "Overview", - }, - { - slug: "v1/someslug#api-protocols-and-headers", - title: "API protocols and headers", - }, - ], - content: - "The Chariot API uses standard HTTP response codes to indicate status and errors. All responses come in standard JSON. The Chariot API is served over HTTPS TLS v1.2+ to ensure data privacy; HTTP and HTTPS with TLS versions below 1.2 are not supported. All requests with a payload must include a Content-Type of application/JSON and the body must be valid JSON.\n\nEvery Chariot API response includes a request_id as the `X-Request-Id` header. The request_id is included whether the API request succeeded or failed. For faster support, include the request_id when contacting support regarding a specific API call.", - indexSegmentId: "testindex", - objectID: undefined, - slug: "v1/someslug#api-protocols-and-headers", - title: "API protocols and headers", - type: "markdown-section-v1", - }, - { - breadcrumbs: [ - { - slug: "v1/someslug", - title: "Overview", - }, - { - slug: "v1/someslug#api-host", - title: "API host", - }, - ], - content: - "```js Server.js\nhttps://sandboxapi.givechariot.com (Sandbox)\nhttps://api.givechariot.com (Production)\n```\n\nChariot has two environments: Sandbox and Production. The Sandbox environment supports only test data. All activity in the Production environment is real. When you’re getting ready to launch into production, please let us know by emailing [support@givechariot.com](support@givechariot.com) to get your production credentials.", - indexSegmentId: "testindex", - objectID: undefined, - slug: "v1/someslug#api-host", - title: "API host", - type: "markdown-section-v1", - }, - ]); - }); + "Overview" + ), + [], + FdrAPI.IndexSegmentId("testindex"), + FernNavigation.V1.Slug("v1/someslug") + ).map((record) => { + return { + ...record, + objectID: undefined, + }; + }) + ).toEqual([ + { + breadcrumbs: [ + { + slug: "v1/someslug", + title: "Overview", + }, + ], + content: + "The Chariot API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n\nIf you feel like something is missing from our API docs, feel free to create an Issue on our [OpenAPI GitHub repo](https://github.com/chariot-giving/chariot-openapi).\n", + indexSegmentId: "testindex", + objectID: undefined, + slug: "v1/someslug", + title: "Overview", + type: "markdown-section-v1", + }, + { + breadcrumbs: [ + { + slug: "v1/someslug", + title: "Overview", + }, + { + slug: "v1/someslug#api-protocols-and-headers", + title: "API protocols and headers", + }, + ], + content: + "The Chariot API uses standard HTTP response codes to indicate status and errors. All responses come in standard JSON. The Chariot API is served over HTTPS TLS v1.2+ to ensure data privacy; HTTP and HTTPS with TLS versions below 1.2 are not supported. All requests with a payload must include a Content-Type of application/JSON and the body must be valid JSON.\n\nEvery Chariot API response includes a request_id as the `X-Request-Id` header. The request_id is included whether the API request succeeded or failed. For faster support, include the request_id when contacting support regarding a specific API call.", + indexSegmentId: "testindex", + objectID: undefined, + slug: "v1/someslug#api-protocols-and-headers", + title: "API protocols and headers", + type: "markdown-section-v1", + }, + { + breadcrumbs: [ + { + slug: "v1/someslug", + title: "Overview", + }, + { + slug: "v1/someslug#api-host", + title: "API host", + }, + ], + content: + "```js Server.js\nhttps://sandboxapi.givechariot.com (Sandbox)\nhttps://api.givechariot.com (Production)\n```\n\nChariot has two environments: Sandbox and Production. The Sandbox environment supports only test data. All activity in the Production environment is real. When you’re getting ready to launch into production, please let us know by emailing [support@givechariot.com](support@givechariot.com) to get your production credentials.", + indexSegmentId: "testindex", + objectID: undefined, + slug: "v1/someslug#api-host", + title: "API host", + type: "markdown-section-v1", + }, + ]); + }); - it("should extract headers into a tree from markdown content", () => { - expect( - getMarkdownSectionTree( - ` + it("should extract headers into a tree from markdown content", () => { + expect( + getMarkdownSectionTree( + ` # A this is line A ## B @@ -126,261 +129,262 @@ Chariot has two environments: Sandbox and Production. The Sandbox environment su ## G this is line g `, - "something", - ), - ).toEqual({ - level: 0, - heading: "something", - content: "\n", - children: [ + "something" + ) + ).toEqual({ + level: 0, + heading: "something", + content: "\n", + children: [ + { + level: 1, + heading: "A", + content: "this is line A\n", + children: [ + { + level: 2, + heading: "B", + content: "this is line b\n```\n## somecrap\nfasdfafafdadf\n```\n", + children: [ { - level: 1, - heading: "A", - content: "this is line A\n", - children: [ - { - level: 2, - heading: "B", - content: "this is line b\n```\n## somecrap\nfasdfafafdadf\n```\n", - children: [ - { - level: 3, - heading: "C", - content: "this is line c\n\nthis is line c.2\n\n", - children: [], - }, - { - level: 3, - heading: "D", - content: "this is line d\n", - children: [], - }, - ], - }, - { - level: 2, - heading: "E", - content: "this is line e\n", - children: [ - { - level: 3, - heading: "F", - content: "this is line f\nthis is line f.2\n", - children: [], - }, - ], - }, - { - level: 2, - heading: "G", - content: "this is line g\n\n", - children: [], - }, - ], + level: 3, + heading: "C", + content: "this is line c\n\nthis is line c.2\n\n", + children: [], }, - ], - }); - }); - - it("should flatten the tree", () => { - expect( - getMarkdownSections( { - level: 0, - heading: "", - content: "\n", - children: [ - { - level: 1, - heading: "A heading", - content: "this is line A\n", - children: [ - { - level: 2, - heading: "B", - content: "this is line b\n```\n## somecrap\nfasdfafafdadf\n```\n", - children: [ - { - level: 3, - heading: "C", - content: "this is line c\n\nthis is line c.2\n\n", - children: [], - }, - { - level: 3, - heading: "D", - content: "this is line d\n", - children: [], - }, - ], - }, - { - level: 2, - heading: "E", - content: "this is line e\n", - children: [ - { - level: 3, - heading: "F", - content: "this is line f\nthis is line f.2\n", - children: [], - }, - ], - }, - { - level: 2, - heading: "G", - content: "this is line g\n\n", - children: [], - }, - ], - }, - ], + level: 3, + heading: "D", + content: "this is line d\n", + children: [], }, - [], - FdrAPI.IndexSegmentId("testindex"), - FernNavigation.V1.Slug("v1/someslug"), - ).map((record) => { - return { - ...record, - objectID: undefined, - }; - }), - ).toEqual([ - { - type: "markdown-section-v1", - objectID: undefined, - title: "A heading", - content: "this is line A", - breadcrumbs: [ - { - slug: "v1/someslug#a-heading", - title: "A heading", - }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#a-heading", - }, - { - type: "markdown-section-v1", - objectID: undefined, - title: "B", - content: "this is line b\n```\n## somecrap\nfasdfafafdadf\n```", - breadcrumbs: [ - { - slug: "v1/someslug#a-heading", - title: "A heading", - }, - { - slug: "v1/someslug#b", - title: "B", - }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#b", - }, - { - type: "markdown-section-v1", - objectID: undefined, - title: "C", - content: "this is line c\n\nthis is line c.2", - breadcrumbs: [ - { - slug: "v1/someslug#a-heading", - title: "A heading", - }, - { - slug: "v1/someslug#b", - title: "B", - }, - { - slug: "v1/someslug#c", - title: "C", - }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#c", + ], }, { - type: "markdown-section-v1", - objectID: undefined, - title: "D", - content: "this is line d", - breadcrumbs: [ - { - slug: "v1/someslug#a-heading", - title: "A heading", - }, - { - slug: "v1/someslug#b", - title: "B", - }, - { - slug: "v1/someslug#d", - title: "D", - }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#d", + level: 2, + heading: "E", + content: "this is line e\n", + children: [ + { + level: 3, + heading: "F", + content: "this is line f\nthis is line f.2\n", + children: [], + }, + ], }, { - type: "markdown-section-v1", - objectID: undefined, - title: "E", - content: "this is line e", - breadcrumbs: [ - { - slug: "v1/someslug#a-heading", - title: "A heading", - }, - { - slug: "v1/someslug#e", - title: "E", - }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#e", + level: 2, + heading: "G", + content: "this is line g\n\n", + children: [], }, + ], + }, + ], + }); + }); + + it("should flatten the tree", () => { + expect( + getMarkdownSections( + { + level: 0, + heading: "", + content: "\n", + children: [ { - type: "markdown-section-v1", - objectID: undefined, - title: "F", - content: "this is line f\nthis is line f.2", - breadcrumbs: [ - { - slug: "v1/someslug#a-heading", - title: "A heading", - }, - { - slug: "v1/someslug#e", - title: "E", - }, + level: 1, + heading: "A heading", + content: "this is line A\n", + children: [ + { + level: 2, + heading: "B", + content: + "this is line b\n```\n## somecrap\nfasdfafafdadf\n```\n", + children: [ { - slug: "v1/someslug#f", - title: "F", + level: 3, + heading: "C", + content: "this is line c\n\nthis is line c.2\n\n", + children: [], }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#f", - }, - { - type: "markdown-section-v1", - objectID: undefined, - title: "G", - content: "this is line g", - breadcrumbs: [ { - slug: "v1/someslug#a-heading", - title: "A heading", + level: 3, + heading: "D", + content: "this is line d\n", + children: [], }, + ], + }, + { + level: 2, + heading: "E", + content: "this is line e\n", + children: [ { - slug: "v1/someslug#g", - title: "G", + level: 3, + heading: "F", + content: "this is line f\nthis is line f.2\n", + children: [], }, - ], - indexSegmentId: "testindex", - slug: "v1/someslug#g", + ], + }, + { + level: 2, + heading: "G", + content: "this is line g\n\n", + children: [], + }, + ], }, - ]); - }); + ], + }, + [], + FdrAPI.IndexSegmentId("testindex"), + FernNavigation.V1.Slug("v1/someslug") + ).map((record) => { + return { + ...record, + objectID: undefined, + }; + }) + ).toEqual([ + { + type: "markdown-section-v1", + objectID: undefined, + title: "A heading", + content: "this is line A", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#a-heading", + }, + { + type: "markdown-section-v1", + objectID: undefined, + title: "B", + content: "this is line b\n```\n## somecrap\nfasdfafafdadf\n```", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + { + slug: "v1/someslug#b", + title: "B", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#b", + }, + { + type: "markdown-section-v1", + objectID: undefined, + title: "C", + content: "this is line c\n\nthis is line c.2", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + { + slug: "v1/someslug#b", + title: "B", + }, + { + slug: "v1/someslug#c", + title: "C", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#c", + }, + { + type: "markdown-section-v1", + objectID: undefined, + title: "D", + content: "this is line d", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + { + slug: "v1/someslug#b", + title: "B", + }, + { + slug: "v1/someslug#d", + title: "D", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#d", + }, + { + type: "markdown-section-v1", + objectID: undefined, + title: "E", + content: "this is line e", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + { + slug: "v1/someslug#e", + title: "E", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#e", + }, + { + type: "markdown-section-v1", + objectID: undefined, + title: "F", + content: "this is line f\nthis is line f.2", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + { + slug: "v1/someslug#e", + title: "E", + }, + { + slug: "v1/someslug#f", + title: "F", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#f", + }, + { + type: "markdown-section-v1", + objectID: undefined, + title: "G", + content: "this is line g", + breadcrumbs: [ + { + slug: "v1/someslug#a-heading", + title: "A heading", + }, + { + slug: "v1/someslug#g", + title: "G", + }, + ], + indexSegmentId: "testindex", + slug: "v1/someslug#g", + }, + ]); + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/generate-algolia-search-records/testGenerateAlgoliaSearchRecordsForDocs.test.ts b/servers/fdr/src/__test__/unit-tests/generate-algolia-search-records/testGenerateAlgoliaSearchRecordsForDocs.test.ts index 476e92f40c..0a459907fe 100644 --- a/servers/fdr/src/__test__/unit-tests/generate-algolia-search-records/testGenerateAlgoliaSearchRecordsForDocs.test.ts +++ b/servers/fdr/src/__test__/unit-tests/generate-algolia-search-records/testGenerateAlgoliaSearchRecordsForDocs.test.ts @@ -1,11 +1,11 @@ import { - APIV1Write, - DocsV1Write, - FdrAPI, - SDKSnippetHolder, - convertAPIDefinitionToDb, - convertDocsDefinitionToDb, - visitDbNavigationConfig, + APIV1Write, + DocsV1Write, + FdrAPI, + SDKSnippetHolder, + convertAPIDefinitionToDb, + convertDocsDefinitionToDb, + visitDbNavigationConfig, } from "@fern-api/fdr-sdk"; import { resolve } from "path"; import type { AlgoliaSearchRecord } from "../../../services/algolia"; @@ -15,185 +15,206 @@ import { createMockFdrApplication } from "../../mock"; const FIXTURES_DIR = resolve(__dirname, "fixtures"); const FIXTURES: Fixture[] = [ - { - name: "primer", - }, - { - name: "vellum", - }, - { - name: "candid", - }, - { - name: "humanloop", - }, - { - name: "vapi", - }, - { - name: "nominal", - }, + { + name: "primer", + }, + { + name: "vellum", + }, + { + name: "candid", + }, + { + name: "humanloop", + }, + { + name: "vapi", + }, + { + name: "nominal", + }, ]; function loadDocsDefinition(fixture: Fixture) { - const filePath = resolve(FIXTURES_DIR, fixture.name, "docs.json"); + const filePath = resolve(FIXTURES_DIR, fixture.name, "docs.json"); - return require(filePath) as DocsV1Write.DocsDefinition; + return require(filePath) as DocsV1Write.DocsDefinition; } function loadApiDefinition(fixture: Fixture, id: string) { - const filePath = resolve(FIXTURES_DIR, fixture.name, "apis", `${id}.json`); + const filePath = resolve(FIXTURES_DIR, fixture.name, "apis", `${id}.json`); - return require(filePath) as APIV1Write.ApiDefinition; + return require(filePath) as APIV1Write.ApiDefinition; } type FilteredSearchRecord = Omit; function filterSearchRecord(record: AlgoliaSearchRecord): FilteredSearchRecord { - const { objectID: _, ...rest } = record; - return rest; + const { objectID: _, ...rest } = record; + return rest; } interface Fixture { - name: string; - only?: boolean; + name: string; + only?: boolean; } const EMPTY_SNIPPET_HOLDER = new SDKSnippetHolder({ - snippetsBySdkId: {}, - snippetsConfigWithSdkId: {}, - snippetTemplatesByEndpoint: {}, - snippetsBySdkIdAndEndpointId: {}, - snippetTemplatesByEndpointId: {}, + snippetsBySdkId: {}, + snippetsConfigWithSdkId: {}, + snippetTemplatesByEndpoint: {}, + snippetsBySdkIdAndEndpointId: {}, + snippetTemplatesByEndpointId: {}, }); describe("generateAlgoliaSearchRecordsForDocs", () => { - for (const fixture of FIXTURES) { - const { only = false } = fixture; - (only ? it.only : it)( - JSON.stringify(fixture), - async () => { - const docsDefinition = convertDocsDefinitionToDb({ - writeShape: loadDocsDefinition(fixture), - files: {}, - }); - - const preloadApiDefinitions = () => { - const apiIdDefinitionTuples = docsDefinition.referencedApis.map((id) => { - const apiDef = loadApiDefinition(fixture, id); - return [id, convertAPIDefinitionToDb(apiDef, id, EMPTY_SNIPPET_HOLDER)] as const; - }); - - return Object.fromEntries(apiIdDefinitionTuples); - }; - - const apiDefinitionsById = preloadApiDefinitions(); - const recordsWithoutIds: Omit[] = []; - const navigationConfig = docsDefinition.config.navigation; - const generator = new AlgoliaSearchRecordGeneratorV2({ docsDefinition, apiDefinitionsById }); - - if (navigationConfig == null) { - throw new Error("Navigation config is required for this test"); + for (const fixture of FIXTURES) { + const { only = false } = fixture; + (only ? it.only : it)( + JSON.stringify(fixture), + async () => { + const docsDefinition = convertDocsDefinitionToDb({ + writeShape: loadDocsDefinition(fixture), + files: {}, + }); + + const preloadApiDefinitions = () => { + const apiIdDefinitionTuples = docsDefinition.referencedApis.map( + (id) => { + const apiDef = loadApiDefinition(fixture, id); + return [ + id, + convertAPIDefinitionToDb(apiDef, id, EMPTY_SNIPPET_HOLDER), + ] as const; + } + ); + + return Object.fromEntries(apiIdDefinitionTuples); + }; + + const apiDefinitionsById = preloadApiDefinitions(); + const recordsWithoutIds: Omit[] = []; + const navigationConfig = docsDefinition.config.navigation; + const generator = new AlgoliaSearchRecordGeneratorV2({ + docsDefinition, + apiDefinitionsById, + }); + + if (navigationConfig == null) { + throw new Error("Navigation config is required for this test"); + } + + visitDbNavigationConfig(navigationConfig, { + versioned: (config) => { + config.versions.forEach((v) => { + const indexSegmentRecords = + generator.generateAlgoliaSearchRecordsForSpecificDocsVersion( + v.config, + { + type: "versioned", + id: FdrAPI.IndexSegmentId(`${v.version}-constant`), + searchApiKey: "api_key", + version: { id: v.version, urlSlug: v.urlSlug }, + } + ); + recordsWithoutIds.push( + ...indexSegmentRecords.map(filterSearchRecord) + ); + }); + }, + unversioned: (config) => { + const records = + generator.generateAlgoliaSearchRecordsForSpecificDocsVersion( + config, + { + type: "unversioned", + id: FdrAPI.IndexSegmentId("constant"), + searchApiKey: "api_key", } + ); + recordsWithoutIds.push(...records.map(filterSearchRecord)); + }, + }); - visitDbNavigationConfig(navigationConfig, { - versioned: (config) => { - config.versions.forEach((v) => { - const indexSegmentRecords = generator.generateAlgoliaSearchRecordsForSpecificDocsVersion( - v.config, - { - type: "versioned", - id: FdrAPI.IndexSegmentId(`${v.version}-constant`), - searchApiKey: "api_key", - version: { id: v.version, urlSlug: v.urlSlug }, - }, - ); - recordsWithoutIds.push(...indexSegmentRecords.map(filterSearchRecord)); - }); - }, - unversioned: (config) => { - const records = generator.generateAlgoliaSearchRecordsForSpecificDocsVersion(config, { - type: "unversioned", - id: FdrAPI.IndexSegmentId("constant"), - searchApiKey: "api_key", - }); - recordsWithoutIds.push(...records.map(filterSearchRecord)); - }, - }); - - expect(recordsWithoutIds).toMatchSnapshot(); - }, - 90_000, - ); - } + expect(recordsWithoutIds).toMatchSnapshot(); + }, + 90_000 + ); + } }); describe("generateIndexSegmentsForDefinition", () => { - const indexSegmentManager = new AlgoliaIndexSegmentManagerServiceImpl(createMockFdrApplication({})); - it("should strip spaces from the version ID", () => { - const segments = indexSegmentManager.generateIndexSegmentsForDefinition({ - dbDocsDefinition: { - type: "v3", - pages: {}, - referencedApis: [], - files: {}, + const indexSegmentManager = new AlgoliaIndexSegmentManagerServiceImpl( + createMockFdrApplication({}) + ); + it("should strip spaces from the version ID", () => { + const segments = indexSegmentManager.generateIndexSegmentsForDefinition({ + dbDocsDefinition: { + type: "v3", + pages: {}, + referencedApis: [], + files: {}, + config: { + navigation: { + versions: [ + { + version: FdrAPI.VersionId("version with spaces"), config: { - navigation: { - versions: [ - { - version: FdrAPI.VersionId("version with spaces"), - config: { - items: [], - landingPage: undefined, - }, - urlSlug: undefined, - availability: undefined, - }, - { - version: FdrAPI.VersionId("version with (special) chars"), - config: { - items: [], - landingPage: undefined, - }, - urlSlug: undefined, - availability: undefined, - }, - ], - }, - root: undefined, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - backgroundImage: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - logo: undefined, - logoV2: undefined, - colors: undefined, - colorsV2: undefined, - typography: undefined, + items: [], + landingPage: undefined, }, - jsFiles: undefined, - }, - url: "https://example.com", - }); - - expect(segments.type).toBe("versioned"); - if (segments.type === "versioned") { - expect(segments.configSegmentTuples[0]?.[1].id).toContain("version-with-spaces"); - expect(segments.configSegmentTuples[1]?.[1].id).toContain("version-with-special-chars"); - } + urlSlug: undefined, + availability: undefined, + }, + { + version: FdrAPI.VersionId("version with (special) chars"), + config: { + items: [], + landingPage: undefined, + }, + urlSlug: undefined, + availability: undefined, + }, + ], + }, + root: undefined, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + backgroundImage: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + logo: undefined, + logoV2: undefined, + colors: undefined, + colorsV2: undefined, + typography: undefined, + }, + jsFiles: undefined, + }, + url: "https://example.com", }); + + expect(segments.type).toBe("versioned"); + if (segments.type === "versioned") { + expect(segments.configSegmentTuples[0]?.[1].id).toContain( + "version-with-spaces" + ); + expect(segments.configSegmentTuples[1]?.[1].id).toContain( + "version-with-special-chars" + ); + } + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/noncifySemanticVersion.test.ts b/servers/fdr/src/__test__/unit-tests/noncifySemanticVersion.test.ts index 81d0ae5708..7b8a061357 100644 --- a/servers/fdr/src/__test__/unit-tests/noncifySemanticVersion.test.ts +++ b/servers/fdr/src/__test__/unit-tests/noncifySemanticVersion.test.ts @@ -1,11 +1,17 @@ import { noncifySemanticVersion } from "../../db/generators/noncifySemanticVersion"; describe("noncify semantic release versions", () => { - it("test", () => { - expect(noncifySemanticVersion("1.2.3")).toEqual("00001-00002-00003-15-00000"); + it("test", () => { + expect(noncifySemanticVersion("1.2.3")).toEqual( + "00001-00002-00003-15-00000" + ); - expect(noncifySemanticVersion("1.2.3-rc0")).toEqual("00001-00002-00003-12-00000"); + expect(noncifySemanticVersion("1.2.3-rc0")).toEqual( + "00001-00002-00003-12-00000" + ); - expect(noncifySemanticVersion("100.20.23")).toEqual("00100-00020-00023-15-00000"); - }); + expect(noncifySemanticVersion("100.20.23")).toEqual( + "00100-00020-00023-15-00000" + ); + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/removeVersionFromFullSlug.test.ts b/servers/fdr/src/__test__/unit-tests/removeVersionFromFullSlug.test.ts index 9d8d37c71b..aab4861c4c 100644 --- a/servers/fdr/src/__test__/unit-tests/removeVersionFromFullSlug.test.ts +++ b/servers/fdr/src/__test__/unit-tests/removeVersionFromFullSlug.test.ts @@ -2,21 +2,21 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { NavigationContext } from "../../services/algolia/NavigationContext"; describe("removeVersionFromFullSlug", () => { - it("should remove version from beginning of full slug", () => { - const fullSlug = ["v2", "full-slug", "sub-slug", "v2"]; - const result = new NavigationContext( - { - type: "versioned", - id: FdrAPI.IndexSegmentId("some navigation context id") as any, - searchApiKey: "search api key", - version: { - id: FdrAPI.VersionId("some id") as any, - urlSlug: "v2", - }, - }, - [], - ).withFullSlug(fullSlug); + it("should remove version from beginning of full slug", () => { + const fullSlug = ["v2", "full-slug", "sub-slug", "v2"]; + const result = new NavigationContext( + { + type: "versioned", + id: FdrAPI.IndexSegmentId("some navigation context id") as any, + searchApiKey: "search api key", + version: { + id: FdrAPI.VersionId("some id") as any, + urlSlug: "v2", + }, + }, + [] + ).withFullSlug(fullSlug); - expect(result.path).toBe("full-slug/sub-slug/v2"); - }); + expect(result.path).toBe("full-slug/sub-slug/v2"); + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/transform/testTransformApiDefinitionToDb.test.ts b/servers/fdr/src/__test__/unit-tests/transform/testTransformApiDefinitionToDb.test.ts index 92b2f2c40c..27dc1ab6a3 100644 --- a/servers/fdr/src/__test__/unit-tests/transform/testTransformApiDefinitionToDb.test.ts +++ b/servers/fdr/src/__test__/unit-tests/transform/testTransformApiDefinitionToDb.test.ts @@ -1,59 +1,64 @@ -import { APIV1Write, FdrAPI, SDKSnippetHolder, convertAPIDefinitionToDb } from "@fern-api/fdr-sdk"; +import { + APIV1Write, + FdrAPI, + SDKSnippetHolder, + convertAPIDefinitionToDb, +} from "@fern-api/fdr-sdk"; import { resolve } from "path"; const EMPTY_SNIPPET_HOLDER = new SDKSnippetHolder({ - snippetsBySdkId: {}, - snippetsConfigWithSdkId: {}, - snippetTemplatesByEndpoint: {}, - snippetsBySdkIdAndEndpointId: {}, - snippetTemplatesByEndpointId: {}, + snippetsBySdkId: {}, + snippetsConfigWithSdkId: {}, + snippetTemplatesByEndpoint: {}, + snippetsBySdkIdAndEndpointId: {}, + snippetTemplatesByEndpointId: {}, }); const FIXTURES_DIR = resolve(__dirname, "fixtures"); const FIXTURES: Fixture[] = [ - { - name: "cyclical-1", - }, - { - name: "vellum", - }, - { - name: "string", - }, - { - name: "candid", - }, - { - name: "realtime", - }, + { + name: "cyclical-1", + }, + { + name: "vellum", + }, + { + name: "string", + }, + { + name: "candid", + }, + { + name: "realtime", + }, ]; function loadFdrApiDefinition(fixture: Fixture) { - const filePath = resolve(FIXTURES_DIR, fixture.name, "fdr.json"); + const filePath = resolve(FIXTURES_DIR, fixture.name, "fdr.json"); - return require(filePath) as APIV1Write.ApiDefinition; + return require(filePath) as APIV1Write.ApiDefinition; } interface Fixture { - name: string; - only?: boolean; + name: string; + only?: boolean; } describe("transformApiDefinitionToDb", () => { - for (const fixture of FIXTURES) { - const { only = false } = fixture; - (only ? it.only : it)( - JSON.stringify(fixture), - async () => { - const apiDef = loadFdrApiDefinition(fixture); - const dbApiDefinition = convertAPIDefinitionToDb( - apiDef, - FdrAPI.ApiDefinitionId("id"), - EMPTY_SNIPPET_HOLDER, - ); - expect(dbApiDefinition).toMatchSnapshot(); - }, - 90_000, + for (const fixture of FIXTURES) { + const { only = false } = fixture; + (only ? it.only : it)( + JSON.stringify(fixture), + async () => { + const apiDef = loadFdrApiDefinition(fixture); + const dbApiDefinition = convertAPIDefinitionToDb( + apiDef, + FdrAPI.ApiDefinitionId("id"), + EMPTY_SNIPPET_HOLDER ); - } + expect(dbApiDefinition).toMatchSnapshot(); + }, + 90_000 + ); + } }); diff --git a/servers/fdr/src/__test__/unit-tests/transformApiDefinitionToDb.test.ts b/servers/fdr/src/__test__/unit-tests/transformApiDefinitionToDb.test.ts index 4e2a97c0ee..5c769f4c6a 100644 --- a/servers/fdr/src/__test__/unit-tests/transformApiDefinitionToDb.test.ts +++ b/servers/fdr/src/__test__/unit-tests/transformApiDefinitionToDb.test.ts @@ -1,78 +1,87 @@ -import { APIV1Write, FdrAPI, SDKSnippetHolder, convertAPIDefinitionToDb } from "@fern-api/fdr-sdk"; +import { + APIV1Write, + FdrAPI, + SDKSnippetHolder, + convertAPIDefinitionToDb, +} from "@fern-api/fdr-sdk"; const EMPTY_SNIPPET_HOLDER = new SDKSnippetHolder({ - snippetsBySdkId: {}, - snippetsConfigWithSdkId: {}, - snippetTemplatesByEndpoint: {}, - snippetsBySdkIdAndEndpointId: {}, - snippetTemplatesByEndpointId: {}, + snippetsBySdkId: {}, + snippetsConfigWithSdkId: {}, + snippetTemplatesByEndpoint: {}, + snippetsBySdkIdAndEndpointId: {}, + snippetTemplatesByEndpointId: {}, }); it("api register", async () => { - const apiDefinition: APIV1Write.ApiDefinition = { - rootPackage: { - pointsTo: undefined, - endpoints: [], - subpackages: [], - types: [APIV1Write.TypeId("type_User")], - websockets: undefined, - webhooks: undefined, - }, - types: { - [APIV1Write.TypeId("type_User")]: { - description: "This is some ```markdown```", - name: "User", - shape: { - type: "alias", - value: { - type: "primitive", - value: { - type: "string", - regex: undefined, - minLength: undefined, - maxLength: undefined, - default: undefined, - }, - }, - }, - availability: undefined, + const apiDefinition: APIV1Write.ApiDefinition = { + rootPackage: { + pointsTo: undefined, + endpoints: [], + subpackages: [], + types: [APIV1Write.TypeId("type_User")], + websockets: undefined, + webhooks: undefined, + }, + types: { + [APIV1Write.TypeId("type_User")]: { + description: "This is some ```markdown```", + name: "User", + shape: { + type: "alias", + value: { + type: "primitive", + value: { + type: "string", + regex: undefined, + minLength: undefined, + maxLength: undefined, + default: undefined, }, + }, }, - subpackages: {}, - auth: undefined, - globalHeaders: undefined, - snippetsConfiguration: undefined, - navigation: undefined, - }; - const dbApiDefinition = convertAPIDefinitionToDb(apiDefinition, FdrAPI.ApiDefinitionId("id"), EMPTY_SNIPPET_HOLDER); - expect(dbApiDefinition).toEqual({ - auth: undefined, - id: "id", - rootPackage: { - pointsTo: undefined, - endpoints: [], - subpackages: [], - types: ["type_User"], - webhooks: [], - websockets: [], - }, - subpackages: {}, - types: { - type_User: { - availability: undefined, - description: "This is some ```markdown```", - name: "User", - shape: { - type: "alias", - value: { - type: "primitive", - value: { - type: "string", - }, - }, - }, + availability: undefined, + }, + }, + subpackages: {}, + auth: undefined, + globalHeaders: undefined, + snippetsConfiguration: undefined, + navigation: undefined, + }; + const dbApiDefinition = convertAPIDefinitionToDb( + apiDefinition, + FdrAPI.ApiDefinitionId("id"), + EMPTY_SNIPPET_HOLDER + ); + expect(dbApiDefinition).toEqual({ + auth: undefined, + id: "id", + rootPackage: { + pointsTo: undefined, + endpoints: [], + subpackages: [], + types: ["type_User"], + webhooks: [], + websockets: [], + }, + subpackages: {}, + types: { + type_User: { + availability: undefined, + description: "This is some ```markdown```", + name: "User", + shape: { + type: "alias", + value: { + type: "primitive", + value: { + type: "string", }, + }, }, - hasMultipleBaseUrls: false, - }); + }, + }, + hasMultipleBaseUrls: false, + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/transformEndpointEndpointCall.test.ts b/servers/fdr/src/__test__/unit-tests/transformEndpointEndpointCall.test.ts index ff43d6c5c9..e81b6be768 100644 --- a/servers/fdr/src/__test__/unit-tests/transformEndpointEndpointCall.test.ts +++ b/servers/fdr/src/__test__/unit-tests/transformEndpointEndpointCall.test.ts @@ -1,128 +1,138 @@ -import { APIV1Write, FdrAPI, SDKSnippetHolder, transformExampleEndpointCall } from "@fern-api/fdr-sdk"; +import { + APIV1Write, + FdrAPI, + SDKSnippetHolder, + transformExampleEndpointCall, +} from "@fern-api/fdr-sdk"; const EMPTY_SNIPPET_HOLDER = new SDKSnippetHolder({ - snippetsBySdkId: {}, - snippetsConfigWithSdkId: {}, - snippetTemplatesByEndpoint: {}, - snippetsBySdkIdAndEndpointId: {}, - snippetTemplatesByEndpointId: {}, + snippetsBySdkId: {}, + snippetsConfigWithSdkId: {}, + snippetTemplatesByEndpoint: {}, + snippetsBySdkIdAndEndpointId: {}, + snippetTemplatesByEndpointId: {}, }); describe("transformEndpointEndpointCall", () => { - it("correctly transforms", () => { - const endpointDefinition: APIV1Write.EndpointDefinition = { - id: FdrAPI.EndpointId("endpoint-id"), - description: "This is some ```markdown```", - method: APIV1Write.HttpMethod.Post, - path: { - parts: [ - { type: "literal", value: "/prefix" }, - { type: "pathParameter", value: FdrAPI.PropertyKey("pathParam") }, - { type: "literal", value: "/suffix" }, - ], - pathParameters: [ - { - key: FdrAPI.PropertyKey("pathParam"), - type: { - type: "primitive", - value: { - type: "string", - regex: undefined, - minLength: undefined, - maxLength: undefined, - default: undefined, - }, - }, - description: undefined, - availability: undefined, - }, - ], + it("correctly transforms", () => { + const endpointDefinition: APIV1Write.EndpointDefinition = { + id: FdrAPI.EndpointId("endpoint-id"), + description: "This is some ```markdown```", + method: APIV1Write.HttpMethod.Post, + path: { + parts: [ + { type: "literal", value: "/prefix" }, + { type: "pathParameter", value: FdrAPI.PropertyKey("pathParam") }, + { type: "literal", value: "/suffix" }, + ], + pathParameters: [ + { + key: FdrAPI.PropertyKey("pathParam"), + type: { + type: "primitive", + value: { + type: "string", + regex: undefined, + minLength: undefined, + maxLength: undefined, + default: undefined, + }, }, - queryParameters: [ - { - key: "queryParam", - type: { - type: "primitive", - value: { type: "integer", minimum: undefined, maximum: undefined, default: undefined }, - }, - description: undefined, - availability: undefined, - }, - ], - headers: [ - { - key: "header", - type: { - type: "primitive", - value: { type: "boolean", default: undefined }, - }, - description: undefined, - availability: undefined, - }, - ], - examples: [], - auth: undefined, - defaultEnvironment: undefined, - environments: undefined, - originalEndpointId: undefined, - name: undefined, - request: undefined, - response: undefined, - errors: undefined, - errorsV2: undefined, + description: undefined, availability: undefined, - }; - - const transformed = transformExampleEndpointCall({ - endpointDefinition, - writeShape: { - path: "/prefix/path-param-value/suffix", - pathParameters: { - [FdrAPI.PropertyKey("pathParam")]: "path-param-value", - }, - queryParameters: { - queryParam: 123, - }, - headers: { - header: true, - }, - responseStatusCode: 200, - name: undefined, - requestBody: undefined, - requestBodyV3: undefined, - responseBody: undefined, - responseBodyV3: undefined, - codeSamples: undefined, - description: undefined, + }, + ], + }, + queryParameters: [ + { + key: "queryParam", + type: { + type: "primitive", + value: { + type: "integer", + minimum: undefined, + maximum: undefined, + default: undefined, }, - snippets: EMPTY_SNIPPET_HOLDER, - }); + }, + description: undefined, + availability: undefined, + }, + ], + headers: [ + { + key: "header", + type: { + type: "primitive", + value: { type: "boolean", default: undefined }, + }, + description: undefined, + availability: undefined, + }, + ], + examples: [], + auth: undefined, + defaultEnvironment: undefined, + environments: undefined, + originalEndpointId: undefined, + name: undefined, + request: undefined, + response: undefined, + errors: undefined, + errorsV2: undefined, + availability: undefined, + }; - expect(transformed).toEqual({ - codeExamples: { - goSdk: undefined, - nodeAxios: "", - pythonSdk: undefined, - typescriptSdk: undefined, - }, - codeSamples: [], - description: undefined, - name: undefined, - headers: { - header: true, - }, - path: "/prefix/path-param-value/suffix", - pathParameters: { - pathParam: "path-param-value", - }, - queryParameters: { - queryParam: 123, - }, - requestBody: undefined, - requestBodyV3: undefined, - responseBody: undefined, - responseBodyV3: undefined, - responseStatusCode: 200, - }); + const transformed = transformExampleEndpointCall({ + endpointDefinition, + writeShape: { + path: "/prefix/path-param-value/suffix", + pathParameters: { + [FdrAPI.PropertyKey("pathParam")]: "path-param-value", + }, + queryParameters: { + queryParam: 123, + }, + headers: { + header: true, + }, + responseStatusCode: 200, + name: undefined, + requestBody: undefined, + requestBodyV3: undefined, + responseBody: undefined, + responseBodyV3: undefined, + codeSamples: undefined, + description: undefined, + }, + snippets: EMPTY_SNIPPET_HOLDER, + }); + + expect(transformed).toEqual({ + codeExamples: { + goSdk: undefined, + nodeAxios: "", + pythonSdk: undefined, + typescriptSdk: undefined, + }, + codeSamples: [], + description: undefined, + name: undefined, + headers: { + header: true, + }, + path: "/prefix/path-param-value/suffix", + pathParameters: { + pathParam: "path-param-value", + }, + queryParameters: { + queryParam: 123, + }, + requestBody: undefined, + requestBodyV3: undefined, + responseBody: undefined, + responseBodyV3: undefined, + responseStatusCode: 200, }); + }); }); diff --git a/servers/fdr/src/__test__/unit-tests/truncate.test.ts b/servers/fdr/src/__test__/unit-tests/truncate.test.ts index 7d2dcdaf99..fce79ae07a 100644 --- a/servers/fdr/src/__test__/unit-tests/truncate.test.ts +++ b/servers/fdr/src/__test__/unit-tests/truncate.test.ts @@ -1,8 +1,8 @@ import { truncateToBytes } from "../../util"; it("api register", async () => { - const truncated = truncateToBytes( - `Welcome to Vellum's API documentation! Here you'll find information about the various endpoints available to you, as well as the parameters and responses that they accept and return. + const truncated = truncateToBytes( + `Welcome to Vellum's API documentation! Here you'll find information about the various endpoints available to you, as well as the parameters and responses that they accept and return. We will be exposing more and more of our APIs over time as they stabilize. If there is some action you can perform via the UI that you wish you could perform via API, please let us know and we can expose it here in an unstable state. @@ -13,8 +13,8 @@ it("api register", async () => { Some endpoints are hosted separately from the main Vellum API and therefore have a different base url. If this is the case, they will say so in their description. Unless otherwise specified, all endpoints use https://api.vellum.ai as their base URL.`, - 100, - ); - console.log(truncated); - expect(truncated.length).toEqual(100); + 100 + ); + console.log(truncated); + expect(truncated.length).toEqual(100); }); diff --git a/servers/fdr/src/app/FdrApplication.ts b/servers/fdr/src/app/FdrApplication.ts index 8f5f75dc40..e3ba55010a 100644 --- a/servers/fdr/src/app/FdrApplication.ts +++ b/servers/fdr/src/app/FdrApplication.ts @@ -3,116 +3,124 @@ import winston from "winston"; import { FdrDao } from "../db"; import { AlgoliaServiceImpl, type AlgoliaService } from "../services/algolia"; import { - AlgoliaIndexSegmentDeleterServiceImpl, - type AlgoliaIndexSegmentDeleterService, + AlgoliaIndexSegmentDeleterServiceImpl, + type AlgoliaIndexSegmentDeleterService, } from "../services/algolia-index-segment-deleter"; import { - AlgoliaIndexSegmentManagerServiceImpl, - type AlgoliaIndexSegmentManagerService, + AlgoliaIndexSegmentManagerServiceImpl, + type AlgoliaIndexSegmentManagerService, } from "../services/algolia-index-segment-manager"; import { AuthServiceImpl, type AuthService } from "../services/auth"; import { DatabaseServiceImpl, type DatabaseService } from "../services/db"; -import { DocsDefinitionCache, DocsDefinitionCacheImpl } from "../services/docs-cache/DocsDefinitionCache"; +import { + DocsDefinitionCache, + DocsDefinitionCacheImpl, +} from "../services/docs-cache/DocsDefinitionCache"; import LocalDocsDefinitionStore from "../services/docs-cache/LocalDocsDefinitionStore"; import RedisDocsDefinitionStore from "../services/docs-cache/RedisDocsDefinitionStore"; -import { RevalidatorService, RevalidatorServiceImpl } from "../services/revalidator/RevalidatorService"; +import { + RevalidatorService, + RevalidatorServiceImpl, +} from "../services/revalidator/RevalidatorService"; import { S3ServiceImpl, type S3Service } from "../services/s3"; import { SlackService, SlackServiceImpl } from "../services/slack/SlackService"; import { type FdrConfig } from "./FdrConfig"; export interface FdrServices { - readonly auth: AuthService; - readonly db: DatabaseService; - readonly algolia: AlgoliaService; - readonly algoliaIndexSegmentDeleter: AlgoliaIndexSegmentDeleterService; - readonly algoliaIndexSegmentManager: AlgoliaIndexSegmentManagerService; - readonly s3: S3Service; - readonly slack: SlackService; - readonly revalidator: RevalidatorService; + readonly auth: AuthService; + readonly db: DatabaseService; + readonly algolia: AlgoliaService; + readonly algoliaIndexSegmentDeleter: AlgoliaIndexSegmentDeleterService; + readonly algoliaIndexSegmentManager: AlgoliaIndexSegmentManagerService; + readonly s3: S3Service; + readonly slack: SlackService; + readonly revalidator: RevalidatorService; } export const LOGGER = winston.createLogger({ - level: "info", - format: winston.format.json(), - transports: [ - new winston.transports.Console({ - format: winston.format.json(), - }), - ], + level: "info", + format: winston.format.json(), + transports: [ + new winston.transports.Console({ + format: winston.format.json(), + }), + ], }); export class FdrApplication { - public readonly services: FdrServices; - public readonly dao: FdrDao; - public readonly docsDefinitionCache: DocsDefinitionCache; - public readonly logger = LOGGER; - public readonly redisDatastore; + public readonly services: FdrServices; + public readonly dao: FdrDao; + public readonly docsDefinitionCache: DocsDefinitionCache; + public readonly logger = LOGGER; + public readonly redisDatastore; - public constructor( - public readonly config: FdrConfig, - services?: Partial, - ) { - this.logger = winston.createLogger({ - level: config.logLevel, - format: winston.format.json(), - transports: [ - new winston.transports.Console({ - format: winston.format.json(), - }), - ], - }); - const prisma = new PrismaClient({ - log: ["info", "warn", "error"], - transactionOptions: { - timeout: 15000, - maxWait: 15000, - }, - }); + public constructor( + public readonly config: FdrConfig, + services?: Partial + ) { + this.logger = winston.createLogger({ + level: config.logLevel, + format: winston.format.json(), + transports: [ + new winston.transports.Console({ + format: winston.format.json(), + }), + ], + }); + const prisma = new PrismaClient({ + log: ["info", "warn", "error"], + transactionOptions: { + timeout: 15000, + maxWait: 15000, + }, + }); - this.services = { - auth: services?.auth ?? new AuthServiceImpl(this), - db: services?.db ?? new DatabaseServiceImpl(prisma), - algolia: services?.algolia ?? new AlgoliaServiceImpl(this), - algoliaIndexSegmentDeleter: - services?.algoliaIndexSegmentDeleter ?? new AlgoliaIndexSegmentDeleterServiceImpl(this), - algoliaIndexSegmentManager: - services?.algoliaIndexSegmentManager ?? new AlgoliaIndexSegmentManagerServiceImpl(this), - s3: services?.s3 ?? new S3ServiceImpl(this.config), - slack: services?.slack ?? new SlackServiceImpl(this), - revalidator: services?.revalidator ?? new RevalidatorServiceImpl(), - }; + this.services = { + auth: services?.auth ?? new AuthServiceImpl(this), + db: services?.db ?? new DatabaseServiceImpl(prisma), + algolia: services?.algolia ?? new AlgoliaServiceImpl(this), + algoliaIndexSegmentDeleter: + services?.algoliaIndexSegmentDeleter ?? + new AlgoliaIndexSegmentDeleterServiceImpl(this), + algoliaIndexSegmentManager: + services?.algoliaIndexSegmentManager ?? + new AlgoliaIndexSegmentManagerServiceImpl(this), + s3: services?.s3 ?? new S3ServiceImpl(this.config), + slack: services?.slack ?? new SlackServiceImpl(this), + revalidator: services?.revalidator ?? new RevalidatorServiceImpl(), + }; - this.dao = new FdrDao(prisma); + this.dao = new FdrDao(prisma); - this.redisDatastore = config.redisEnabled - ? new RedisDocsDefinitionStore({ - cacheEndpointUrl: `redis://${this.config.docsCacheEndpoint}`, - clusterModeEnabled: config.redisClusteringEnabled, - }) - : undefined; + this.redisDatastore = config.redisEnabled + ? new RedisDocsDefinitionStore({ + cacheEndpointUrl: `redis://${this.config.docsCacheEndpoint}`, + clusterModeEnabled: config.redisClusteringEnabled, + }) + : undefined; - this.docsDefinitionCache = new DocsDefinitionCacheImpl( - this, - this.dao, - new LocalDocsDefinitionStore(), - this.redisDatastore, - ); + this.docsDefinitionCache = new DocsDefinitionCacheImpl( + this, + this.dao, + new LocalDocsDefinitionStore(), + this.redisDatastore + ); - if ("prepareStackTrace" in Error) { - Error.prepareStackTrace = (err, stack) => - JSON.stringify({ - message: err.message, - stack: stack.map((frame) => ({ - file: frame.getFileName(), - function: frame.getFunctionName(), - column: frame.getColumnNumber(), - line: frame.getLineNumber(), - })), - }); - } + if ("prepareStackTrace" in Error) { + Error.prepareStackTrace = (err, stack) => + JSON.stringify({ + message: err.message, + stack: stack.map((frame) => ({ + file: frame.getFileName(), + function: frame.getFunctionName(), + column: frame.getColumnNumber(), + line: frame.getLineNumber(), + })), + }); } + } - public async initialize(): Promise { - await this.docsDefinitionCache.initialize(); - } + public async initialize(): Promise { + await this.docsDefinitionCache.initialize(); + } } diff --git a/servers/fdr/src/app/FdrConfig.ts b/servers/fdr/src/app/FdrConfig.ts index 747b157ebf..c083cf325e 100644 --- a/servers/fdr/src/app/FdrConfig.ts +++ b/servers/fdr/src/app/FdrConfig.ts @@ -10,9 +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 API_DEFINITION_SOURCE_BUCKET_NAME_ENV_VAR = "API_DEFINITION_SOURCE_BUCKET_NAME"; -const API_DEFINITION_SOURCE_BUCKET_REGION_ENV_VAR = "API_DEFINITION_SOURCE_BUCKET_REGION"; -const API_DEFINITION_SOURCE_BUCKET_URL_OVERRIDE_ENV_VAR = "API_DEFINITION_SOURCE_BUCKET_URL_OVERRIDE"; +const API_DEFINITION_SOURCE_BUCKET_NAME_ENV_VAR = + "API_DEFINITION_SOURCE_BUCKET_NAME"; +const API_DEFINITION_SOURCE_BUCKET_REGION_ENV_VAR = + "API_DEFINITION_SOURCE_BUCKET_REGION"; +const API_DEFINITION_SOURCE_BUCKET_URL_OVERRIDE_ENV_VAR = + "API_DEFINITION_SOURCE_BUCKET_URL_OVERRIDE"; const DOMAIN_SUFFIX_ENV_VAR = "DOMAIN_SUFFIX"; const ALGOLIA_APP_ID_ENV_VAR = "ALGOLIA_APP_ID"; @@ -30,75 +33,100 @@ const APPLICATION_ENVIRONMENT_ENV_VAR = "APPLICATION_ENVIRONMENT"; const PUBLIC_DOCS_CDN_URL = "PUBLIC_DOCS_CDN_URL"; export interface S3Config { - bucketName: string; - bucketRegion: string; - urlOverride?: string; + bucketName: string; + bucketRegion: string; + urlOverride?: string; } export interface FdrConfig { - venusUrl: string; - awsAccessKey: string; - awsSecretKey: string; - cdnPublicDocsUrl: string; - publicDocsS3: S3Config; - privateDocsS3: S3Config; - privateApiDefinitionSourceS3: S3Config; - domainSuffix: string; - algoliaAppId: string; - algoliaAdminApiKey: string; - algoliaSearchApiKey: string; - algoliaSearchIndex: string; - algoliaSearchV2Domains: string[]; - slackToken: string; - logLevel: string; - docsCacheEndpoint: string; - enableCustomerNotifications: boolean; - redisEnabled: boolean; - redisClusteringEnabled: boolean; - applicationEnvironment: string; + venusUrl: string; + awsAccessKey: string; + awsSecretKey: string; + cdnPublicDocsUrl: string; + publicDocsS3: S3Config; + privateDocsS3: S3Config; + privateApiDefinitionSourceS3: S3Config; + domainSuffix: string; + algoliaAppId: string; + algoliaAdminApiKey: string; + algoliaSearchApiKey: string; + algoliaSearchIndex: string; + algoliaSearchV2Domains: string[]; + slackToken: string; + logLevel: string; + docsCacheEndpoint: string; + enableCustomerNotifications: boolean; + redisEnabled: boolean; + redisClusteringEnabled: boolean; + applicationEnvironment: string; } export function getConfig(): FdrConfig { - return { - venusUrl: getEnvironmentVariableOrThrow(VENUS_URL_ENV_VAR), - awsAccessKey: getEnvironmentVariableOrThrow(AWS_ACCESS_KEY_ENV_VAR), - awsSecretKey: getEnvironmentVariableOrThrow(AWS_SECRET_KEY_ENV_VAR), - publicDocsS3: { - bucketName: getEnvironmentVariableOrThrow(PUBLIC_S3_BUCKET_NAME_ENV_VAR), - bucketRegion: getEnvironmentVariableOrThrow(PUBLIC_S3_BUCKET_REGION_ENV_VAR), - urlOverride: process.env[PUBLIC_S3_URL_OVERRIDE_ENV_VAR], - }, - privateDocsS3: { - bucketName: getEnvironmentVariableOrThrow(PRIVATE_S3_BUCKET_NAME_ENV_VAR), - bucketRegion: getEnvironmentVariableOrThrow(PRIVATE_S3_BUCKET_REGION_ENV_VAR), - urlOverride: process.env[PRIVATE_S3_URL_OVERRIDE_ENV_VAR], - }, - privateApiDefinitionSourceS3: { - bucketName: getEnvironmentVariableOrThrow(API_DEFINITION_SOURCE_BUCKET_NAME_ENV_VAR), - bucketRegion: getEnvironmentVariableOrThrow(API_DEFINITION_SOURCE_BUCKET_REGION_ENV_VAR), - urlOverride: process.env[API_DEFINITION_SOURCE_BUCKET_URL_OVERRIDE_ENV_VAR], - }, - domainSuffix: getEnvironmentVariableOrThrow(DOMAIN_SUFFIX_ENV_VAR), - algoliaAppId: getEnvironmentVariableOrThrow(ALGOLIA_APP_ID_ENV_VAR), - algoliaAdminApiKey: getEnvironmentVariableOrThrow(ALGOLIA_ADMIN_API_KEY_ENV_VAR), - algoliaSearchIndex: getEnvironmentVariableOrThrow(ALGOLIA_SEARCH_INDEX_ENV_VAR), - algoliaSearchApiKey: getEnvironmentVariableOrThrow(ALGOLIA_SEARCH_API_KEY_ENV_VAR), - algoliaSearchV2Domains: getEnvironmentVariableOrThrow(ALGOLIA_SEARCH_V2_DOMAINS_ENV_VAR).split(",") ?? [], - slackToken: getEnvironmentVariableOrThrow(SLACK_TOKEN_ENV_VAR), - logLevel: process.env[LOG_LEVEL_ENV_VAR] ?? "info", - docsCacheEndpoint: getEnvironmentVariableOrThrow(DOCS_CACHE_ENDPOINT_ENV_VAR), - enableCustomerNotifications: getEnvironmentVariableOrThrow(ENABLE_CUSTOMER_NOTIFICATIONS_ENV_VAR) === "true", - redisEnabled: process.env[REDIS_ENABLED_ENV_VAR] === "true", - redisClusteringEnabled: process.env[REDIS_CLUSTERING_ENABLED_ENV_VAR] === "true", - applicationEnvironment: getEnvironmentVariableOrThrow(APPLICATION_ENVIRONMENT_ENV_VAR), - cdnPublicDocsUrl: getEnvironmentVariableOrThrow(PUBLIC_DOCS_CDN_URL), - }; + return { + venusUrl: getEnvironmentVariableOrThrow(VENUS_URL_ENV_VAR), + awsAccessKey: getEnvironmentVariableOrThrow(AWS_ACCESS_KEY_ENV_VAR), + awsSecretKey: getEnvironmentVariableOrThrow(AWS_SECRET_KEY_ENV_VAR), + publicDocsS3: { + bucketName: getEnvironmentVariableOrThrow(PUBLIC_S3_BUCKET_NAME_ENV_VAR), + bucketRegion: getEnvironmentVariableOrThrow( + PUBLIC_S3_BUCKET_REGION_ENV_VAR + ), + urlOverride: process.env[PUBLIC_S3_URL_OVERRIDE_ENV_VAR], + }, + privateDocsS3: { + bucketName: getEnvironmentVariableOrThrow(PRIVATE_S3_BUCKET_NAME_ENV_VAR), + bucketRegion: getEnvironmentVariableOrThrow( + PRIVATE_S3_BUCKET_REGION_ENV_VAR + ), + urlOverride: process.env[PRIVATE_S3_URL_OVERRIDE_ENV_VAR], + }, + privateApiDefinitionSourceS3: { + bucketName: getEnvironmentVariableOrThrow( + API_DEFINITION_SOURCE_BUCKET_NAME_ENV_VAR + ), + bucketRegion: getEnvironmentVariableOrThrow( + API_DEFINITION_SOURCE_BUCKET_REGION_ENV_VAR + ), + urlOverride: + process.env[API_DEFINITION_SOURCE_BUCKET_URL_OVERRIDE_ENV_VAR], + }, + domainSuffix: getEnvironmentVariableOrThrow(DOMAIN_SUFFIX_ENV_VAR), + algoliaAppId: getEnvironmentVariableOrThrow(ALGOLIA_APP_ID_ENV_VAR), + algoliaAdminApiKey: getEnvironmentVariableOrThrow( + ALGOLIA_ADMIN_API_KEY_ENV_VAR + ), + algoliaSearchIndex: getEnvironmentVariableOrThrow( + ALGOLIA_SEARCH_INDEX_ENV_VAR + ), + algoliaSearchApiKey: getEnvironmentVariableOrThrow( + ALGOLIA_SEARCH_API_KEY_ENV_VAR + ), + algoliaSearchV2Domains: + getEnvironmentVariableOrThrow(ALGOLIA_SEARCH_V2_DOMAINS_ENV_VAR).split( + "," + ) ?? [], + slackToken: getEnvironmentVariableOrThrow(SLACK_TOKEN_ENV_VAR), + logLevel: process.env[LOG_LEVEL_ENV_VAR] ?? "info", + docsCacheEndpoint: getEnvironmentVariableOrThrow( + DOCS_CACHE_ENDPOINT_ENV_VAR + ), + enableCustomerNotifications: + getEnvironmentVariableOrThrow(ENABLE_CUSTOMER_NOTIFICATIONS_ENV_VAR) === + "true", + redisEnabled: process.env[REDIS_ENABLED_ENV_VAR] === "true", + redisClusteringEnabled: + process.env[REDIS_CLUSTERING_ENABLED_ENV_VAR] === "true", + applicationEnvironment: getEnvironmentVariableOrThrow( + APPLICATION_ENVIRONMENT_ENV_VAR + ), + cdnPublicDocsUrl: getEnvironmentVariableOrThrow(PUBLIC_DOCS_CDN_URL), + }; } function getEnvironmentVariableOrThrow(environmentVariable: string): string { - const value = process.env[environmentVariable]; - if (value == null) { - throw new Error(`Environment variable ${environmentVariable} not found`); - } - return value; + const value = process.env[environmentVariable]; + if (value == null) { + throw new Error(`Environment variable ${environmentVariable} not found`); + } + return value; } diff --git a/servers/fdr/src/background.ts b/servers/fdr/src/background.ts index 314a7fd05e..293bdbf329 100644 --- a/servers/fdr/src/background.ts +++ b/servers/fdr/src/background.ts @@ -2,20 +2,26 @@ import * as cron from "node-cron"; import { type FdrApplication } from "./app"; export function registerBackgroundTasks(app: FdrApplication) { - registerAlgoliaSearchRecordDeletionBackgroundTask(app); + registerAlgoliaSearchRecordDeletionBackgroundTask(app); } -export function registerAlgoliaSearchRecordDeletionBackgroundTask(app: FdrApplication) { - // Runs every 10 minutes - cron.schedule("*/10 * * * *", async () => { - try { - const deletedIndexSegmentCount = - await app.services.algoliaIndexSegmentDeleter.deleteOldInactiveIndexSegments({ - olderThanHours: 24, - }); - app.logger.debug(`Successfully deleted ${deletedIndexSegmentCount} old index segments.`); - } catch (e) { - app.logger.error("Error while deleting old index segments.", e); - } - }); +export function registerAlgoliaSearchRecordDeletionBackgroundTask( + app: FdrApplication +) { + // Runs every 10 minutes + cron.schedule("*/10 * * * *", async () => { + try { + const deletedIndexSegmentCount = + await app.services.algoliaIndexSegmentDeleter.deleteOldInactiveIndexSegments( + { + olderThanHours: 24, + } + ); + app.logger.debug( + `Successfully deleted ${deletedIndexSegmentCount} old index segments.` + ); + } catch (e) { + app.logger.error("Error while deleting old index segments.", e); + } + }); } diff --git a/servers/fdr/src/controllers/api/getApiReadService.ts b/servers/fdr/src/controllers/api/getApiReadService.ts index 030a80ba99..1cfaca8e4a 100644 --- a/servers/fdr/src/controllers/api/getApiReadService.ts +++ b/servers/fdr/src/controllers/api/getApiReadService.ts @@ -5,33 +5,37 @@ import { ApiDoesNotExistError } from "../../api/generated/api/resources/api/reso import type { FdrApplication } from "../../app"; export function getReadApiService(app: FdrApplication): APIV1ReadService { - return new APIV1ReadService({ - getApi: async (req, res) => { - try { - // if the auth header belongs to fern, return the api definition - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - } catch (e) { - if (e instanceof UserNotInOrgError) { - const orgId = await app.dao.apis().getOrgIdForApiDefinition(req.params.apiDefinitionId); - if (orgId == null) { - throw new ApiDoesNotExistError(); - } - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId, - }); - } - throw e; - } - const dbApiDefinition = await app.dao.apis().loadAPIDefinition(req.params.apiDefinitionId); - if (dbApiDefinition == null) { - throw new ApiDoesNotExistError(); - } - const readApiDefinition = convertDbAPIDefinitionToRead(dbApiDefinition); - return res.send(readApiDefinition); - }, - }); + return new APIV1ReadService({ + getApi: async (req, res) => { + try { + // if the auth header belongs to fern, return the api definition + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + } catch (e) { + if (e instanceof UserNotInOrgError) { + const orgId = await app.dao + .apis() + .getOrgIdForApiDefinition(req.params.apiDefinitionId); + if (orgId == null) { + throw new ApiDoesNotExistError(); + } + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId, + }); + } + throw e; + } + const dbApiDefinition = await app.dao + .apis() + .loadAPIDefinition(req.params.apiDefinitionId); + if (dbApiDefinition == null) { + throw new ApiDoesNotExistError(); + } + const readApiDefinition = convertDbAPIDefinitionToRead(dbApiDefinition); + return res.send(readApiDefinition); + }, + }); } diff --git a/servers/fdr/src/controllers/api/getRegisterApiService.ts b/servers/fdr/src/controllers/api/getRegisterApiService.ts index 650cf6c03d..4ff38c3a4b 100644 --- a/servers/fdr/src/controllers/api/getRegisterApiService.ts +++ b/servers/fdr/src/controllers/api/getRegisterApiService.ts @@ -1,278 +1,317 @@ -import { APIV1Write, FdrAPI, SDKSnippetHolder, convertAPIDefinitionToDb } from "@fern-api/fdr-sdk"; +import { + APIV1Write, + FdrAPI, + SDKSnippetHolder, + convertAPIDefinitionToDb, +} from "@fern-api/fdr-sdk"; import { v4 as uuidv4 } from "uuid"; import { APIV1WriteService } from "../../api"; import { SdkRequest } from "../../api/generated/api"; import type { FdrApplication } from "../../app"; import { LOGGER } from "../../app/FdrApplication"; import { SdkIdForPackage } from "../../db/sdk/SdkDao"; -import { SnippetTemplatesByEndpoint, SnippetTemplatesByEndpointIdentifier } from "../../db/snippets/SnippetTemplate"; +import { + SnippetTemplatesByEndpoint, + SnippetTemplatesByEndpointIdentifier, +} from "../../db/snippets/SnippetTemplate"; import { writeBuffer } from "../../util"; const REGISTER_API_DEFINITION_META = { - service: "APIV1WriteService", - endpoint: "registerApiDefinition", + service: "APIV1WriteService", + endpoint: "registerApiDefinition", }; export function getRegisterApiService(app: FdrApplication): APIV1WriteService { - return new APIV1WriteService({ - registerApiDefinition: async (req, res) => { - app.logger.debug(`Checking if user belongs to org ${req.body.orgId}`, REGISTER_API_DEFINITION_META); - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: req.body.orgId, - }); - const snippetsConfiguration = req.body.definition.snippetsConfiguration ?? { - typescriptSdk: undefined, - pythonSdk: undefined, - javaSdk: undefined, - goSdk: undefined, - rubySdk: undefined, - }; + return new APIV1WriteService({ + registerApiDefinition: async (req, res) => { + app.logger.debug( + `Checking if user belongs to org ${req.body.orgId}`, + REGISTER_API_DEFINITION_META + ); + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: req.body.orgId, + }); + const snippetsConfiguration = req.body.definition + .snippetsConfiguration ?? { + typescriptSdk: undefined, + pythonSdk: undefined, + javaSdk: undefined, + goSdk: undefined, + rubySdk: undefined, + }; - const snippetsConfigurationWithSdkIds = await app.dao.sdks().getSdkIdsForPackages(snippetsConfiguration); - const sdkIds: string[] = []; - if (snippetsConfigurationWithSdkIds.typescriptSdk != null) { - sdkIds.push(snippetsConfigurationWithSdkIds.typescriptSdk.sdkId); - } - if (snippetsConfigurationWithSdkIds.pythonSdk != null) { - sdkIds.push(snippetsConfigurationWithSdkIds.pythonSdk.sdkId); - } - if (snippetsConfigurationWithSdkIds.javaSdk != null) { - sdkIds.push(snippetsConfigurationWithSdkIds.javaSdk.sdkId); - } - if (snippetsConfigurationWithSdkIds.goSdk != null) { - sdkIds.push(snippetsConfigurationWithSdkIds.goSdk.sdkId); - } - if (snippetsConfigurationWithSdkIds.rubySdk != null) { - sdkIds.push(snippetsConfigurationWithSdkIds.rubySdk.sdkId); - } + const snippetsConfigurationWithSdkIds = await app.dao + .sdks() + .getSdkIdsForPackages(snippetsConfiguration); + const sdkIds: string[] = []; + if (snippetsConfigurationWithSdkIds.typescriptSdk != null) { + sdkIds.push(snippetsConfigurationWithSdkIds.typescriptSdk.sdkId); + } + if (snippetsConfigurationWithSdkIds.pythonSdk != null) { + sdkIds.push(snippetsConfigurationWithSdkIds.pythonSdk.sdkId); + } + if (snippetsConfigurationWithSdkIds.javaSdk != null) { + sdkIds.push(snippetsConfigurationWithSdkIds.javaSdk.sdkId); + } + if (snippetsConfigurationWithSdkIds.goSdk != null) { + sdkIds.push(snippetsConfigurationWithSdkIds.goSdk.sdkId); + } + if (snippetsConfigurationWithSdkIds.rubySdk != null) { + sdkIds.push(snippetsConfigurationWithSdkIds.rubySdk.sdkId); + } - const snippetsBySdkId = await app.dao.snippets().loadAllSnippetsForSdkIds(sdkIds); - const snippetsBySdkIdAndEndpointId = await app.dao.snippets().loadAllSnippetsForSdkIdsByEndpointId(sdkIds); - const snippetTemplatesByEndpoint = await getSnippetTemplatesIfEnabled({ - app, - authorization: req.headers.authorization, - orgId: req.body.orgId, - apiId: req.body.apiId, - definition: req.body.definition, - snippetsConfigurationWithSdkIds, - }); - const snippetTemplatesByEndpointId = await getSnippetTemplatesByEndpointIdIfEnabled({ - app, - authorization: req.headers.authorization, - orgId: req.body.orgId, - apiId: req.body.apiId, - definition: req.body.definition, - snippetsConfigurationWithSdkIds, - }); - const apiDefinitionId = FdrAPI.ApiDefinitionId(uuidv4()); - const snippetHolder = new SDKSnippetHolder({ - snippetsBySdkId, - snippetsBySdkIdAndEndpointId, - snippetsConfigWithSdkId: snippetsConfigurationWithSdkIds, - snippetTemplatesByEndpoint, - snippetTemplatesByEndpointId, - }); - const transformedApiDefinition = convertAPIDefinitionToDb( - req.body.definition, - apiDefinitionId, - snippetHolder, - ); + const snippetsBySdkId = await app.dao + .snippets() + .loadAllSnippetsForSdkIds(sdkIds); + const snippetsBySdkIdAndEndpointId = await app.dao + .snippets() + .loadAllSnippetsForSdkIdsByEndpointId(sdkIds); + const snippetTemplatesByEndpoint = await getSnippetTemplatesIfEnabled({ + app, + authorization: req.headers.authorization, + orgId: req.body.orgId, + apiId: req.body.apiId, + definition: req.body.definition, + snippetsConfigurationWithSdkIds, + }); + const snippetTemplatesByEndpointId = + await getSnippetTemplatesByEndpointIdIfEnabled({ + app, + authorization: req.headers.authorization, + orgId: req.body.orgId, + apiId: req.body.apiId, + definition: req.body.definition, + snippetsConfigurationWithSdkIds, + }); + const apiDefinitionId = FdrAPI.ApiDefinitionId(uuidv4()); + const snippetHolder = new SDKSnippetHolder({ + snippetsBySdkId, + snippetsBySdkIdAndEndpointId, + snippetsConfigWithSdkId: snippetsConfigurationWithSdkIds, + snippetTemplatesByEndpoint, + snippetTemplatesByEndpointId, + }); + const transformedApiDefinition = convertAPIDefinitionToDb( + req.body.definition, + apiDefinitionId, + snippetHolder + ); - let sources: Record | undefined; - if (req.body.sources != null) { - app.logger.debug( - `Preparing source upload URLs for {orgId: "${req.body.orgId}", apiId: "${req.body.apiId}"}`, - REGISTER_API_DEFINITION_META, - ); - sources = await getSourceUploads({ - app, - orgId: req.body.orgId, - apiId: req.body.apiId, - sources: req.body.sources, - }); - app.logger.debug("Successfully prepared source upload URLs", REGISTER_API_DEFINITION_META); - } + let sources: Record | undefined; + if (req.body.sources != null) { + app.logger.debug( + `Preparing source upload URLs for {orgId: "${req.body.orgId}", apiId: "${req.body.apiId}"}`, + REGISTER_API_DEFINITION_META + ); + sources = await getSourceUploads({ + app, + orgId: req.body.orgId, + apiId: req.body.apiId, + sources: req.body.sources, + }); + app.logger.debug( + "Successfully prepared source upload URLs", + REGISTER_API_DEFINITION_META + ); + } - app.logger.debug( - `Creating API Definition in database with name=${req.body.apiId} for org ${req.body.orgId}`, - REGISTER_API_DEFINITION_META, - ); - await app.services.db.prisma.apiDefinitionsV2.create({ - data: { - apiDefinitionId, - apiName: req.body.apiId, - orgId: req.body.orgId, - definition: writeBuffer(transformedApiDefinition), - }, - }); - app.logger.debug(`Returning API Definition ID id=${apiDefinitionId}`, REGISTER_API_DEFINITION_META); - return res.send({ - apiDefinitionId, - sources, - }); + app.logger.debug( + `Creating API Definition in database with name=${req.body.apiId} for org ${req.body.orgId}`, + REGISTER_API_DEFINITION_META + ); + await app.services.db.prisma.apiDefinitionsV2.create({ + data: { + apiDefinitionId, + apiName: req.body.apiId, + orgId: req.body.orgId, + definition: writeBuffer(transformedApiDefinition), }, - }); + }); + app.logger.debug( + `Returning API Definition ID id=${apiDefinitionId}`, + REGISTER_API_DEFINITION_META + ); + return res.send({ + apiDefinitionId, + sources, + }); + }, + }); } function getSnippetSdkRequests({ - snippetsConfigurationWithSdkIds, + snippetsConfigurationWithSdkIds, }: { - snippetsConfigurationWithSdkIds: SdkIdForPackage; + snippetsConfigurationWithSdkIds: SdkIdForPackage; }): SdkRequest[] { - const sdkRequests: SdkRequest[] = []; - if (snippetsConfigurationWithSdkIds.typescriptSdk != null) { - sdkRequests.push({ - type: "typescript", - package: snippetsConfigurationWithSdkIds.typescriptSdk.package, - version: snippetsConfigurationWithSdkIds.typescriptSdk.version, - }); - } - if (snippetsConfigurationWithSdkIds.pythonSdk != null) { - sdkRequests.push({ - type: "python", - package: snippetsConfigurationWithSdkIds.pythonSdk.package, - version: snippetsConfigurationWithSdkIds.pythonSdk.version, - }); - } - if (snippetsConfigurationWithSdkIds.javaSdk != null) { - const coordinate = snippetsConfigurationWithSdkIds.javaSdk.coordinate; - const [group, artifact] = coordinate.split(":"); - if (group == null || artifact == null) { - throw new Error(`Invalid coordinate for Java SDK: ${coordinate}. Must be in the format group:artifact`); - } - sdkRequests.push({ - type: "java", - group, - artifact, - version: snippetsConfigurationWithSdkIds.javaSdk.version, - }); - } - if (snippetsConfigurationWithSdkIds.goSdk != null) { - sdkRequests.push({ - type: "go", - githubRepo: snippetsConfigurationWithSdkIds.goSdk.githubRepo, - version: snippetsConfigurationWithSdkIds.goSdk.version, - }); - } - if (snippetsConfigurationWithSdkIds.rubySdk != null) { - sdkRequests.push({ - type: "ruby", - gem: snippetsConfigurationWithSdkIds.rubySdk.gem, - version: snippetsConfigurationWithSdkIds.rubySdk.version, - }); + const sdkRequests: SdkRequest[] = []; + if (snippetsConfigurationWithSdkIds.typescriptSdk != null) { + sdkRequests.push({ + type: "typescript", + package: snippetsConfigurationWithSdkIds.typescriptSdk.package, + version: snippetsConfigurationWithSdkIds.typescriptSdk.version, + }); + } + if (snippetsConfigurationWithSdkIds.pythonSdk != null) { + sdkRequests.push({ + type: "python", + package: snippetsConfigurationWithSdkIds.pythonSdk.package, + version: snippetsConfigurationWithSdkIds.pythonSdk.version, + }); + } + if (snippetsConfigurationWithSdkIds.javaSdk != null) { + const coordinate = snippetsConfigurationWithSdkIds.javaSdk.coordinate; + const [group, artifact] = coordinate.split(":"); + if (group == null || artifact == null) { + throw new Error( + `Invalid coordinate for Java SDK: ${coordinate}. Must be in the format group:artifact` + ); } - return sdkRequests; + sdkRequests.push({ + type: "java", + group, + artifact, + version: snippetsConfigurationWithSdkIds.javaSdk.version, + }); + } + if (snippetsConfigurationWithSdkIds.goSdk != null) { + sdkRequests.push({ + type: "go", + githubRepo: snippetsConfigurationWithSdkIds.goSdk.githubRepo, + version: snippetsConfigurationWithSdkIds.goSdk.version, + }); + } + if (snippetsConfigurationWithSdkIds.rubySdk != null) { + sdkRequests.push({ + type: "ruby", + gem: snippetsConfigurationWithSdkIds.rubySdk.gem, + version: snippetsConfigurationWithSdkIds.rubySdk.version, + }); + } + return sdkRequests; } async function getSnippetTemplatesByEndpointIdIfEnabled({ - app, - authorization, - orgId, - apiId, - definition, - snippetsConfigurationWithSdkIds, + app, + authorization, + orgId, + apiId, + definition, + snippetsConfigurationWithSdkIds, }: { - app: FdrApplication; - authorization: string | undefined; - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - definition: APIV1Write.ApiDefinition; - snippetsConfigurationWithSdkIds: SdkIdForPackage; + app: FdrApplication; + authorization: string | undefined; + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + definition: APIV1Write.ApiDefinition; + snippetsConfigurationWithSdkIds: SdkIdForPackage; }): Promise { - try { - const hasSnippetTemplatesAccess = await app.services.auth.checkOrgHasSnippetTemplateAccess({ - authHeader: authorization, - orgId, - failHard: false, + try { + const hasSnippetTemplatesAccess = + await app.services.auth.checkOrgHasSnippetTemplateAccess({ + authHeader: authorization, + orgId, + failHard: false, + }); + let snippetTemplatesByEndpoint: SnippetTemplatesByEndpointIdentifier = {}; + if (hasSnippetTemplatesAccess) { + const sdkRequests = getSnippetSdkRequests({ + snippetsConfigurationWithSdkIds, + }); + snippetTemplatesByEndpoint = await app.dao + .snippetTemplates() + .loadSnippetTemplatesByEndpointIdentifier({ + orgId, + apiId, + sdkRequests, + definition, }); - let snippetTemplatesByEndpoint: SnippetTemplatesByEndpointIdentifier = {}; - if (hasSnippetTemplatesAccess) { - const sdkRequests = getSnippetSdkRequests({ snippetsConfigurationWithSdkIds }); - snippetTemplatesByEndpoint = await app.dao.snippetTemplates().loadSnippetTemplatesByEndpointIdentifier({ - orgId, - apiId, - sdkRequests, - definition, - }); - } - return snippetTemplatesByEndpoint; - } catch (e) { - LOGGER.error("Failed to load snippet templates", e); - return {}; } + return snippetTemplatesByEndpoint; + } catch (e) { + LOGGER.error("Failed to load snippet templates", e); + return {}; + } } async function getSnippetTemplatesIfEnabled({ - app, - authorization, - orgId, - apiId, - definition, - snippetsConfigurationWithSdkIds, + app, + authorization, + orgId, + apiId, + definition, + snippetsConfigurationWithSdkIds, }: { - app: FdrApplication; - authorization: string | undefined; - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - definition: APIV1Write.ApiDefinition; - snippetsConfigurationWithSdkIds: SdkIdForPackage; + app: FdrApplication; + authorization: string | undefined; + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + definition: APIV1Write.ApiDefinition; + snippetsConfigurationWithSdkIds: SdkIdForPackage; }): Promise { - try { - const hasSnippetTemplatesAccess = await app.services.auth.checkOrgHasSnippetTemplateAccess({ - authHeader: authorization, - orgId, - failHard: false, + try { + const hasSnippetTemplatesAccess = + await app.services.auth.checkOrgHasSnippetTemplateAccess({ + authHeader: authorization, + orgId, + failHard: false, + }); + let snippetTemplatesByEndpoint: SnippetTemplatesByEndpoint = {}; + if (hasSnippetTemplatesAccess) { + const sdkRequests = getSnippetSdkRequests({ + snippetsConfigurationWithSdkIds, + }); + snippetTemplatesByEndpoint = await app.dao + .snippetTemplates() + .loadSnippetTemplatesByEndpoint({ + orgId, + apiId, + sdkRequests, + definition, }); - let snippetTemplatesByEndpoint: SnippetTemplatesByEndpoint = {}; - if (hasSnippetTemplatesAccess) { - const sdkRequests = getSnippetSdkRequests({ snippetsConfigurationWithSdkIds }); - snippetTemplatesByEndpoint = await app.dao.snippetTemplates().loadSnippetTemplatesByEndpoint({ - orgId, - apiId, - sdkRequests, - definition, - }); - } - return snippetTemplatesByEndpoint; - } catch (e) { - LOGGER.error("Failed to load snippet templates", e); - return {}; } + return snippetTemplatesByEndpoint; + } catch (e) { + LOGGER.error("Failed to load snippet templates", e); + return {}; + } } async function getSourceUploads({ - app, - orgId, - apiId, - sources, + app, + orgId, + apiId, + sources, }: { - app: FdrApplication; - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - sources: Record; + app: FdrApplication; + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + sources: Record; }): Promise> { - const sourceUploadUrls = await app.services.s3.getPresignedApiDefinitionSourceUploadUrls({ - orgId, - apiId, - sources, + const sourceUploadUrls = + await app.services.s3.getPresignedApiDefinitionSourceUploadUrls({ + orgId, + apiId, + sources, }); - const sourceUploads = await Promise.all( - Object.entries(sourceUploadUrls).map(async ([sourceId, fileInfo]) => { - const downloadUrl = await app.services.s3.getPresignedApiDefinitionSourceDownloadUrl({ - key: fileInfo.key, - }); + const sourceUploads = await Promise.all( + Object.entries(sourceUploadUrls).map(async ([sourceId, fileInfo]) => { + const downloadUrl = + await app.services.s3.getPresignedApiDefinitionSourceDownloadUrl({ + key: fileInfo.key, + }); - return [ - sourceId, - { - uploadUrl: fileInfo.presignedUrl, - downloadUrl, - }, - ]; - }), - ); + return [ + sourceId, + { + uploadUrl: fileInfo.presignedUrl, + downloadUrl, + }, + ]; + }) + ); - return Object.fromEntries(sourceUploads); + return Object.fromEntries(sourceUploads); } diff --git a/servers/fdr/src/controllers/diff/getApiDiffService.ts b/servers/fdr/src/controllers/diff/getApiDiffService.ts index 3a1ca023b3..0df9e455b1 100644 --- a/servers/fdr/src/controllers/diff/getApiDiffService.ts +++ b/servers/fdr/src/controllers/diff/getApiDiffService.ts @@ -1,207 +1,232 @@ import { APIV1Db, FdrAPI } from "@fern-api/fdr-sdk"; import { DiffService } from "../../api"; -import { PathParameter, PathParameterDiff, QueryParameter, QueryParameterDiff } from "../../api/generated/api"; +import { + PathParameter, + PathParameterDiff, + QueryParameter, + QueryParameterDiff, +} from "../../api/generated/api"; import { ApiDoesNotExistError } from "../../api/generated/api/resources/api/resources/v1/resources/read/errors"; import { type FdrApplication } from "../../app"; export function getApiDiffService(app: FdrApplication): DiffService { - return new DiffService({ - diff: async (req, res) => { - const previous = await app.dao.apis().loadAPIDefinition(req.query.previousApiDefinitionId); - const current = await app.dao.apis().loadAPIDefinition(req.query.currentApiDefinitionId); - - if (!previous || !current) { - throw new ApiDoesNotExistError(); - } - - const previousEndpoints = getEndpoints(previous); - const currentEndpoints = getEndpoints(current); - - const visitedEndpoints = new Set(); - const addedEndpoints: FdrAPI.AddedEndpoint[] = []; - const updatedEndpoints: FdrAPI.UpdatedEndpoint[] = []; - for (const [endpointId, currentEndpoint] of Object.entries(currentEndpoints)) { - const previousEndpoint = previousEndpoints[FdrAPI.EndpointId(endpointId)]; - const endpointIdentifier = { - method: currentEndpoint.method, - path: getEndpointPath(currentEndpoint), - identifierOverride: currentEndpoint.id, - }; - if (previousEndpoint == null) { - addedEndpoints.push({ - id: endpointIdentifier, - }); - continue; - } else { - const endpointDiff: FdrAPI.UpdatedEndpoint = { - id: endpointIdentifier, - pathParameterDiff: getPathParameterDiff({ - previous: previousEndpoint, - current: currentEndpoint, - }), - queryParameterDiff: getQueryParameterDiff({ - previous: previousEndpoint, - current: currentEndpoint, - }), - requestBodyDiff: { - added: [], - removed: [], - }, - responseBodyDiff: { - added: [], - removed: [], - }, - }; - updatedEndpoints.push(endpointDiff); - } - visitedEndpoints.add(endpointId); - } - return res.send({ - addedEndpoints, - updatedEndpoints, - removedEndpoints: [], - markdown: generateMarkdownChangelog({ added: addedEndpoints, updated: updatedEndpoints }), - }); - }, - }); + return new DiffService({ + diff: async (req, res) => { + const previous = await app.dao + .apis() + .loadAPIDefinition(req.query.previousApiDefinitionId); + const current = await app.dao + .apis() + .loadAPIDefinition(req.query.currentApiDefinitionId); + + if (!previous || !current) { + throw new ApiDoesNotExistError(); + } + + const previousEndpoints = getEndpoints(previous); + const currentEndpoints = getEndpoints(current); + + const visitedEndpoints = new Set(); + const addedEndpoints: FdrAPI.AddedEndpoint[] = []; + const updatedEndpoints: FdrAPI.UpdatedEndpoint[] = []; + for (const [endpointId, currentEndpoint] of Object.entries( + currentEndpoints + )) { + const previousEndpoint = + previousEndpoints[FdrAPI.EndpointId(endpointId)]; + const endpointIdentifier = { + method: currentEndpoint.method, + path: getEndpointPath(currentEndpoint), + identifierOverride: currentEndpoint.id, + }; + if (previousEndpoint == null) { + addedEndpoints.push({ + id: endpointIdentifier, + }); + continue; + } else { + const endpointDiff: FdrAPI.UpdatedEndpoint = { + id: endpointIdentifier, + pathParameterDiff: getPathParameterDiff({ + previous: previousEndpoint, + current: currentEndpoint, + }), + queryParameterDiff: getQueryParameterDiff({ + previous: previousEndpoint, + current: currentEndpoint, + }), + requestBodyDiff: { + added: [], + removed: [], + }, + responseBodyDiff: { + added: [], + removed: [], + }, + }; + updatedEndpoints.push(endpointDiff); + } + visitedEndpoints.add(endpointId); + } + return res.send({ + addedEndpoints, + updatedEndpoints, + removedEndpoints: [], + markdown: generateMarkdownChangelog({ + added: addedEndpoints, + updated: updatedEndpoints, + }), + }); + }, + }); } function generateMarkdownChangelog({ - added, - updated, + added, + updated, }: { - added: FdrAPI.AddedEndpoint[]; - updated: FdrAPI.UpdatedEndpoint[]; + added: FdrAPI.AddedEndpoint[]; + updated: FdrAPI.UpdatedEndpoint[]; }): string { - let markdown = ""; - if (added.length > 0) { - markdown += `The following endpoints were added: + let markdown = ""; + if (added.length > 0) { + markdown += `The following endpoints were added: `; - for (const addEndpont of added) { - markdown += ` - \`${addEndpont.id.method} ${addEndpont.id.path}\` \n`; - } + for (const addEndpont of added) { + markdown += ` - \`${addEndpont.id.method} ${addEndpont.id.path}\` \n`; } + } - if (updated.length > 0) { - markdown += `The following endpoints were updated: + if (updated.length > 0) { + markdown += `The following endpoints were updated: `; - for (const updateEndpoint of updated) { - const newPathParams: string[] = []; - if (updateEndpoint.pathParameterDiff.added.length > 0) { - updateEndpoint.pathParameterDiff.added.map((param) => { - newPathParams.push(param.wireKey); - }); - } - - const newQueryParams: string[] = []; - if (updateEndpoint.pathParameterDiff.added.length > 0) { - updateEndpoint.pathParameterDiff.added.map((param) => { - newQueryParams.push(param.wireKey); - }); - } - - let withUpdates = "with "; - let addAnd = false; - if (newPathParams.length > 0) { - withUpdates += `query parameters ${newQueryParams.map((param) => `\`${param}\``).join(", ")}`; - addAnd = true; - } - - if (newPathParams.length > 0) { - if (addAnd) { - withUpdates += " and "; - } - withUpdates += `path parameters ${newPathParams.map((param) => `\`${param}\``).join(", ")}`; - } - - markdown += ` - ${updateEndpoint.id.method} ${updateEndpoint.id.path} ${withUpdates}. `; + for (const updateEndpoint of updated) { + const newPathParams: string[] = []; + if (updateEndpoint.pathParameterDiff.added.length > 0) { + updateEndpoint.pathParameterDiff.added.map((param) => { + newPathParams.push(param.wireKey); + }); + } + + const newQueryParams: string[] = []; + if (updateEndpoint.pathParameterDiff.added.length > 0) { + updateEndpoint.pathParameterDiff.added.map((param) => { + newQueryParams.push(param.wireKey); + }); + } + + let withUpdates = "with "; + let addAnd = false; + if (newPathParams.length > 0) { + withUpdates += `query parameters ${newQueryParams.map((param) => `\`${param}\``).join(", ")}`; + addAnd = true; + } + + if (newPathParams.length > 0) { + if (addAnd) { + withUpdates += " and "; } - } + withUpdates += `path parameters ${newPathParams.map((param) => `\`${param}\``).join(", ")}`; + } - for (const addedEndpoint of added) { - markdown += ` - ${addedEndpoint.id.method} ${addedEndpoint.id.path}`; + markdown += ` - ${updateEndpoint.id.method} ${updateEndpoint.id.path} ${withUpdates}. `; } + } + + for (const addedEndpoint of added) { + markdown += ` - ${addedEndpoint.id.method} ${addedEndpoint.id.path}`; + } - return markdown; + return markdown; } function getPathParameterDiff({ - previous, - current, + previous, + current, }: { - previous: APIV1Db.DbEndpointDefinition; - current: APIV1Db.DbEndpointDefinition; + previous: APIV1Db.DbEndpointDefinition; + current: APIV1Db.DbEndpointDefinition; }): PathParameterDiff { - const added: PathParameter[] = []; - const removed: PathParameter[] = []; - for (const currentParameter of current.path.pathParameters) { - const previousParameter = previous.path.pathParameters.find((maybePreviousParameter) => { - maybePreviousParameter.key === currentParameter.key; - }); - if (previousParameter == null) { - added.push({ - wireKey: currentParameter.key, - }); - } + const added: PathParameter[] = []; + const removed: PathParameter[] = []; + for (const currentParameter of current.path.pathParameters) { + const previousParameter = previous.path.pathParameters.find( + (maybePreviousParameter) => { + maybePreviousParameter.key === currentParameter.key; + } + ); + if (previousParameter == null) { + added.push({ + wireKey: currentParameter.key, + }); } - return { - added, - removed, - }; + } + return { + added, + removed, + }; } function getQueryParameterDiff({ - previous, - current, + previous, + current, }: { - previous: APIV1Db.DbEndpointDefinition; - current: APIV1Db.DbEndpointDefinition; + previous: APIV1Db.DbEndpointDefinition; + current: APIV1Db.DbEndpointDefinition; }): QueryParameterDiff { - const added: QueryParameter[] = []; - const removed: QueryParameter[] = []; - for (const currentParameter of current.path.pathParameters) { - const previousParameter = previous.path.pathParameters.find((maybePreviousParameter) => { - maybePreviousParameter.key === currentParameter.key; - }); - if (previousParameter == null) { - added.push({ - wireKey: currentParameter.key, - }); - } + const added: QueryParameter[] = []; + const removed: QueryParameter[] = []; + for (const currentParameter of current.path.pathParameters) { + const previousParameter = previous.path.pathParameters.find( + (maybePreviousParameter) => { + maybePreviousParameter.key === currentParameter.key; + } + ); + if (previousParameter == null) { + added.push({ + wireKey: currentParameter.key, + }); } - return { - added, - removed, - }; + } + return { + added, + removed, + }; } -function getEndpoints(apiDefinition: APIV1Db.DbApiDefinition): Record { - const endpoints: Record = {}; - apiDefinition.rootPackage.endpoints.forEach((endpoint) => { - endpoints[getEndpointId(endpoint)] = endpoint; - }); - Object.values(apiDefinition.subpackages).forEach((subpackage) => { - subpackage.endpoints.forEach((endpoint) => { - endpoints[getEndpointId(endpoint)] = endpoint; - }); +function getEndpoints( + apiDefinition: APIV1Db.DbApiDefinition +): Record { + const endpoints: Record = {}; + apiDefinition.rootPackage.endpoints.forEach((endpoint) => { + endpoints[getEndpointId(endpoint)] = endpoint; + }); + Object.values(apiDefinition.subpackages).forEach((subpackage) => { + subpackage.endpoints.forEach((endpoint) => { + endpoints[getEndpointId(endpoint)] = endpoint; }); - return endpoints; + }); + return endpoints; } -function getEndpointId(endpoint: APIV1Db.DbEndpointDefinition): FdrAPI.EndpointId { - const path = getEndpointPath(endpoint); - return FdrAPI.EndpointId(`${endpoint.method}_${path}`); +function getEndpointId( + endpoint: APIV1Db.DbEndpointDefinition +): FdrAPI.EndpointId { + const path = getEndpointPath(endpoint); + return FdrAPI.EndpointId(`${endpoint.method}_${path}`); } -function getEndpointPath(endpoint: APIV1Db.DbEndpointDefinition): FdrAPI.EndpointPathLiteral { - let path = ""; - for (const part of endpoint.path.parts) { - if (part.type === "literal") { - path += part.value; - } else { - path += `{${part.value}}`; - } +function getEndpointPath( + endpoint: APIV1Db.DbEndpointDefinition +): FdrAPI.EndpointPathLiteral { + let path = ""; + for (const part of endpoint.path.parts) { + if (part.type === "literal") { + path += part.value; + } else { + path += `{${part.value}}`; } - return FdrAPI.EndpointPathLiteral(path); + } + return FdrAPI.EndpointPathLiteral(path); } diff --git a/servers/fdr/src/controllers/docs-cache/getDocsCacheService.ts b/servers/fdr/src/controllers/docs-cache/getDocsCacheService.ts index 0f68132f19..3930651bbe 100644 --- a/servers/fdr/src/controllers/docs-cache/getDocsCacheService.ts +++ b/servers/fdr/src/controllers/docs-cache/getDocsCacheService.ts @@ -3,10 +3,12 @@ import { type FdrApplication } from "../../app"; import { ParsedBaseUrl } from "../../util/ParsedBaseUrl"; export function getDocsCacheService(app: FdrApplication): DocsCacheService { - return new DocsCacheService({ - invalidate: async (req, res) => { - await app.docsDefinitionCache.invalidateCache(ParsedBaseUrl.parse(req.body.url).toURL()); - return res.send(); - }, - }); + return new DocsCacheService({ + invalidate: async (req, res) => { + await app.docsDefinitionCache.invalidateCache( + ParsedBaseUrl.parse(req.body.url).toURL() + ); + return res.send(); + }, + }); } diff --git a/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts b/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts index 93cbeabb65..2c91d044eb 100644 --- a/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts +++ b/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts @@ -1,15 +1,15 @@ import { - APIV1Db, - APIV1Read, - Algolia, - DocsV1Db, - DocsV1Read, - FdrAPI, - FernNavigation, - convertDbAPIDefinitionToRead, - convertDocsDefinitionToRead, - migrateDocsDbDefinition, - visitDbNavigationConfig, + APIV1Db, + APIV1Read, + Algolia, + DocsV1Db, + DocsV1Read, + FdrAPI, + FernNavigation, + convertDbAPIDefinitionToRead, + convertDocsDefinitionToRead, + migrateDocsDbDefinition, + visitDbNavigationConfig, } from "@fern-api/fdr-sdk"; import { AuthType, type IndexSegment } from "@prisma/client"; import { keyBy } from "es-toolkit/array"; @@ -23,257 +23,287 @@ import { readBuffer } from "../../../util"; import { getFilesV2 } from "../../../util/getFilesV2"; export function getDocsReadService(app: FdrApplication): DocsV1ReadService { - return new DocsV1ReadService({ - getDocsForDomainLegacy: async (req, res) => { - // TODO: start deleting this deprecated endpoint - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - const definition = await getDocsForDomain({ app, domain: req.params.domain }); - return res.send(definition.response); - }, - getDocsForDomain: async (req, res) => { - // TODO: start deleting this deprecated endpoint - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - const definition = await getDocsForDomain({ app, domain: req.body.domain }); - return res.send(definition.response); - }, - }); + return new DocsV1ReadService({ + getDocsForDomainLegacy: async (req, res) => { + // TODO: start deleting this deprecated endpoint + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + const definition = await getDocsForDomain({ + app, + domain: req.params.domain, + }); + return res.send(definition.response); + }, + getDocsForDomain: async (req, res) => { + // TODO: start deleting this deprecated endpoint + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + const definition = await getDocsForDomain({ + app, + domain: req.body.domain, + }); + return res.send(definition.response); + }, + }); } export async function getDocsForDomain({ - app, - domain, + app, + domain, }: { - app: FdrApplication; - domain: string; -}): Promise<{ response: DocsV1Read.DocsDefinition; dbFiles?: Record }> { - const [docs, docsV2] = await Promise.all([ - app.services.db.prisma.docs.findFirst({ - where: { - url: domain, - }, - }), - app.services.db.prisma.docsV2.findFirst({ - where: { - domain, - }, - }), - ]); + app: FdrApplication; + domain: string; +}): Promise<{ + response: DocsV1Read.DocsDefinition; + dbFiles?: Record; +}> { + const [docs, docsV2] = await Promise.all([ + app.services.db.prisma.docs.findFirst({ + where: { + url: domain, + }, + }), + app.services.db.prisma.docsV2.findFirst({ + where: { + domain, + }, + }), + ]); - if (!docs) { - throw new DomainNotRegisteredError(); - } - const docsDefinitionJson = readBuffer(docs.docsDefinition); - const docsDbDefinition = migrateDocsDbDefinition(docsDefinitionJson); + if (!docs) { + throw new DomainNotRegisteredError(); + } + const docsDefinitionJson = readBuffer(docs.docsDefinition); + const docsDbDefinition = migrateDocsDbDefinition(docsDefinitionJson); - if (docsV2 != null && docsV2.authType !== AuthType.PUBLIC) { - throw new UnauthorizedError("You must be authorized to view this documentation."); - } + if (docsV2 != null && docsV2.authType !== AuthType.PUBLIC) { + throw new UnauthorizedError( + "You must be authorized to view this documentation." + ); + } - return { - response: await getDocsDefinition({ - app, - docsDbDefinition, - docsV2: - docsV2 != null - ? { - algoliaIndex: - docsV2.algoliaIndex != null - ? FdrAPI.algolia.AlgoliaSearchIndex(docsV2.algoliaIndex) - : undefined, - orgId: FdrAPI.OrgId(docsV2.orgID), - docsDefinition: migrateDocsDbDefinition(readBuffer(docsV2.docsDefinition)), - docsConfigInstanceId: - docsV2.docsConfigInstanceId != null - ? FdrAPI.DocsConfigId(docsV2.docsConfigInstanceId) - : null, - indexSegmentIds: docsV2.indexSegmentIds as string[], - path: docsV2.path, - domain: docsV2.domain, - updatedTime: docsV2.updatedTime, - authType: docsV2.authType, - hasPublicS3Assets: docsV2.hasPublicS3Assets, - isPreview: docsV2.isPreview, - } - : null, - }), - dbFiles: docsDbDefinition.files, - }; + return { + response: await getDocsDefinition({ + app, + docsDbDefinition, + docsV2: + docsV2 != null + ? { + algoliaIndex: + docsV2.algoliaIndex != null + ? FdrAPI.algolia.AlgoliaSearchIndex(docsV2.algoliaIndex) + : undefined, + orgId: FdrAPI.OrgId(docsV2.orgID), + docsDefinition: migrateDocsDbDefinition( + readBuffer(docsV2.docsDefinition) + ), + docsConfigInstanceId: + docsV2.docsConfigInstanceId != null + ? FdrAPI.DocsConfigId(docsV2.docsConfigInstanceId) + : null, + indexSegmentIds: docsV2.indexSegmentIds as string[], + path: docsV2.path, + domain: docsV2.domain, + updatedTime: docsV2.updatedTime, + authType: docsV2.authType, + hasPublicS3Assets: docsV2.hasPublicS3Assets, + isPreview: docsV2.isPreview, + } + : null, + }), + dbFiles: docsDbDefinition.files, + }; } export async function getDocsDefinition({ - app, - docsDbDefinition, - docsV2, + app, + docsDbDefinition, + docsV2, }: { - app: FdrApplication; - docsDbDefinition: DocsV1Db.DocsDefinitionDb; - docsV2: LoadDocsDefinitionByUrlResponse | null; + app: FdrApplication; + docsDbDefinition: DocsV1Db.DocsDefinitionDb; + docsV2: LoadDocsDefinitionByUrlResponse | null; }): Promise { - const [apiDefinitions, searchInfo] = await Promise.all([ - app.services.db.prisma.apiDefinitionsV2.findMany({ - where: { - apiDefinitionId: { - in: Array.from(docsDbDefinition.referencedApis), - }, - }, - }), - loadIndexSegmentsAndGetSearchInfo({ - app, - docsDbDefinition, - docsV2, - }), - ]); + const [apiDefinitions, searchInfo] = await Promise.all([ + app.services.db.prisma.apiDefinitionsV2.findMany({ + where: { + apiDefinitionId: { + in: Array.from(docsDbDefinition.referencedApis), + }, + }, + }), + loadIndexSegmentsAndGetSearchInfo({ + app, + docsDbDefinition, + docsV2, + }), + ]); - const bufferedApiDefinitionsById = keyBy(apiDefinitions, (def) => DocsV1Db.ApiDefinitionId(def.apiDefinitionId)); + const bufferedApiDefinitionsById = keyBy(apiDefinitions, (def) => + DocsV1Db.ApiDefinitionId(def.apiDefinitionId) + ); - const filesV2 = await getFilesV2(docsDbDefinition, app); + const filesV2 = await getFilesV2(docsDbDefinition, app); - const apiDefinitionsById = mapValues(bufferedApiDefinitionsById, (def) => - convertDbApiDefinitionToRead(def.definition), - ); + const apiDefinitionsById = mapValues(bufferedApiDefinitionsById, (def) => + convertDbApiDefinitionToRead(def.definition) + ); - return convertDocsDefinitionToRead({ - docsDbDefinition, - algoliaSearchIndex: docsV2?.algoliaIndex ?? undefined, - filesV2, - apis: apiDefinitionsById, - id: docsV2?.docsConfigInstanceId ?? undefined, - search: searchInfo, - }); + return convertDocsDefinitionToRead({ + docsDbDefinition, + algoliaSearchIndex: docsV2?.algoliaIndex ?? undefined, + filesV2, + apis: apiDefinitionsById, + id: docsV2?.docsConfigInstanceId ?? undefined, + search: searchInfo, + }); } export async function loadIndexSegmentsAndGetSearchInfo({ - app, - docsDbDefinition, - docsV2, + app, + docsDbDefinition, + docsV2, }: { - app: FdrApplication; - docsDbDefinition: DocsV1Db.DocsDefinitionDb; - docsV2: LoadDocsDefinitionByUrlResponse | null; + app: FdrApplication; + docsDbDefinition: DocsV1Db.DocsDefinitionDb; + docsV2: LoadDocsDefinitionByUrlResponse | null; }): Promise { - const activeIndexSegments = - docsV2?.indexSegmentIds != null - ? await app.services.db.prisma.indexSegment.findMany({ - where: { id: { in: docsV2.indexSegmentIds } }, - }) - : []; - return getSearchInfoFromDocs({ - algoliaIndex: docsV2?.algoliaIndex, - indexSegmentIds: docsV2?.indexSegmentIds, - docsDbDefinition, - activeIndexSegments, - app, - }); + const activeIndexSegments = + docsV2?.indexSegmentIds != null + ? await app.services.db.prisma.indexSegment.findMany({ + where: { id: { in: docsV2.indexSegmentIds } }, + }) + : []; + return getSearchInfoFromDocs({ + algoliaIndex: docsV2?.algoliaIndex, + indexSegmentIds: docsV2?.indexSegmentIds, + docsDbDefinition, + activeIndexSegments, + app, + }); } -function getVersionedSearchInfoFromDocs(activeIndexSegments: IndexSegment[], app: FdrApplication): Algolia.SearchInfo { - const indexSegmentsByVersionId = activeIndexSegments.reduce>( - (acc, indexSegment) => { - const searchApiKey = app.services.algoliaIndexSegmentManager.getOrGenerateSearchApiKeyForIndexSegment( - indexSegment.id, - ); - // Since the docs are versioned, all referenced index segments will have a version - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - acc[indexSegment.version!] = { - id: FdrAPI.IndexSegmentId(indexSegment.id), - searchApiKey, - }; - return acc; - }, - {}, - ); - return { - type: "singleAlgoliaIndex", - value: { - type: "versioned", - indexSegmentsByVersionId, - }, +function getVersionedSearchInfoFromDocs( + activeIndexSegments: IndexSegment[], + app: FdrApplication +): Algolia.SearchInfo { + const indexSegmentsByVersionId = activeIndexSegments.reduce< + Record + >((acc, indexSegment) => { + const searchApiKey = + app.services.algoliaIndexSegmentManager.getOrGenerateSearchApiKeyForIndexSegment( + indexSegment.id + ); + // Since the docs are versioned, all referenced index segments will have a version + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + acc[indexSegment.version!] = { + id: FdrAPI.IndexSegmentId(indexSegment.id), + searchApiKey, }; + return acc; + }, {}); + return { + type: "singleAlgoliaIndex", + value: { + type: "versioned", + indexSegmentsByVersionId, + }, + }; } function getUnversionedSearchInfoFromDocs( - activeIndexSegments: IndexSegment[], - app: FdrApplication, + activeIndexSegments: IndexSegment[], + app: FdrApplication ): Algolia.SearchInfo { - const indexSegment = activeIndexSegments[0]; - if (indexSegment == null) { - /* preview docs do not have algolia index segments, and should return with an undefined index */ - return { type: "legacyMultiAlgoliaIndex", algoliaIndex: undefined }; - } - const searchApiKey = app.services.algoliaIndexSegmentManager.getOrGenerateSearchApiKeyForIndexSegment( - indexSegment.id, + const indexSegment = activeIndexSegments[0]; + if (indexSegment == null) { + /* preview docs do not have algolia index segments, and should return with an undefined index */ + return { type: "legacyMultiAlgoliaIndex", algoliaIndex: undefined }; + } + const searchApiKey = + app.services.algoliaIndexSegmentManager.getOrGenerateSearchApiKeyForIndexSegment( + indexSegment.id ); - return { - type: "singleAlgoliaIndex", - value: { - type: "unversioned", - indexSegment: { - id: FdrAPI.IndexSegmentId(indexSegment.id), - searchApiKey, - }, - }, - }; + return { + type: "singleAlgoliaIndex", + value: { + type: "unversioned", + indexSegment: { + id: FdrAPI.IndexSegmentId(indexSegment.id), + searchApiKey, + }, + }, + }; } export function getSearchInfoFromDocs({ - algoliaIndex, - indexSegmentIds, - activeIndexSegments, - docsDbDefinition, - app, + algoliaIndex, + indexSegmentIds, + activeIndexSegments, + docsDbDefinition, + app, }: { - algoliaIndex: string | undefined; - indexSegmentIds: string[] | undefined; - activeIndexSegments: IndexSegment[]; - docsDbDefinition: DocsV1Db.DocsDefinitionDb; - app: FdrApplication; + algoliaIndex: string | undefined; + indexSegmentIds: string[] | undefined; + activeIndexSegments: IndexSegment[]; + docsDbDefinition: DocsV1Db.DocsDefinitionDb; + app: FdrApplication; }): Algolia.SearchInfo { - if (indexSegmentIds == null) { - return { type: "legacyMultiAlgoliaIndex", algoliaIndex }; - } + if (indexSegmentIds == null) { + return { type: "legacyMultiAlgoliaIndex", algoliaIndex }; + } - if (docsDbDefinition.config.navigation == null && docsDbDefinition.config.root == null) { - return { type: "legacyMultiAlgoliaIndex", algoliaIndex }; - } + if ( + docsDbDefinition.config.navigation == null && + docsDbDefinition.config.root == null + ) { + return { type: "legacyMultiAlgoliaIndex", algoliaIndex }; + } - if (docsDbDefinition.config.root != null) { - const latestRoot = FernNavigation.migrate.FernNavigationV1ToLatest.create().root(docsDbDefinition.config.root); - let searchInfo: Algolia.SearchInfo | undefined; - FernNavigation.traverseBF(latestRoot, (node) => { - if (node.type === "versioned") { - searchInfo = getVersionedSearchInfoFromDocs(activeIndexSegments, app); - return false; - } else if (node.type === "unversioned") { - searchInfo = getUnversionedSearchInfoFromDocs(activeIndexSegments, app); - return false; - } - return true; - }); - if (searchInfo != null) { - return searchInfo; - } - } else if (docsDbDefinition.config.navigation != null) { - return visitDbNavigationConfig(docsDbDefinition.config.navigation, { - versioned: () => { - return getVersionedSearchInfoFromDocs(activeIndexSegments, app); - }, - unversioned: () => { - return getUnversionedSearchInfoFromDocs(activeIndexSegments, app); - }, - }); + if (docsDbDefinition.config.root != null) { + const latestRoot = + FernNavigation.migrate.FernNavigationV1ToLatest.create().root( + docsDbDefinition.config.root + ); + let searchInfo: Algolia.SearchInfo | undefined; + FernNavigation.traverseBF(latestRoot, (node) => { + if (node.type === "versioned") { + searchInfo = getVersionedSearchInfoFromDocs(activeIndexSegments, app); + return false; + } else if (node.type === "unversioned") { + searchInfo = getUnversionedSearchInfoFromDocs(activeIndexSegments, app); + return false; + } + return true; + }); + if (searchInfo != null) { + return searchInfo; } + } else if (docsDbDefinition.config.navigation != null) { + return visitDbNavigationConfig( + docsDbDefinition.config.navigation, + { + versioned: () => { + return getVersionedSearchInfoFromDocs(activeIndexSegments, app); + }, + unversioned: () => { + return getUnversionedSearchInfoFromDocs(activeIndexSegments, app); + }, + } + ); + } - return { type: "legacyMultiAlgoliaIndex", algoliaIndex }; + return { type: "legacyMultiAlgoliaIndex", algoliaIndex }; } -export function convertDbApiDefinitionToRead(buffer: Buffer): APIV1Read.ApiDefinition { - const apiDefinitionJson = readBuffer(buffer) as APIV1Db.DbApiDefinition; - return convertDbAPIDefinitionToRead(apiDefinitionJson); +export function convertDbApiDefinitionToRead( + buffer: Buffer +): APIV1Read.ApiDefinition { + const apiDefinitionJson = readBuffer(buffer) as APIV1Db.DbApiDefinition; + return convertDbAPIDefinitionToRead(apiDefinitionJson); } diff --git a/servers/fdr/src/controllers/docs/v1/getDocsWriteService.ts b/servers/fdr/src/controllers/docs/v1/getDocsWriteService.ts index 01998b5b12..d51e6f978b 100644 --- a/servers/fdr/src/controllers/docs/v1/getDocsWriteService.ts +++ b/servers/fdr/src/controllers/docs/v1/getDocsWriteService.ts @@ -1,4 +1,8 @@ -import { DocsV1Write, FdrAPI, convertDocsDefinitionToDb } from "@fern-api/fdr-sdk"; +import { + DocsV1Write, + FdrAPI, + convertDocsDefinitionToDb, +} from "@fern-api/fdr-sdk"; import { v4 as uuidv4 } from "uuid"; import { DocsV1WriteService } from "../../../api"; import type { FdrApplication } from "../../../app"; @@ -6,75 +10,80 @@ import { type S3DocsFileInfo } from "../../../services/s3"; import { writeBuffer } from "../../../util"; import { DocsRegistrationIdNotFound } from "../../../api/generated/api/resources/docs/resources/v1/resources/write/errors"; -const DOCS_REGISTRATIONS: Record = {}; +const DOCS_REGISTRATIONS: Record< + DocsV1Write.DocsRegistrationId, + DocsRegistrationInfo +> = {}; interface DocsRegistrationInfo { - domain: string; - orgId: FdrAPI.OrgId; - s3FileInfos: Record; + domain: string; + orgId: FdrAPI.OrgId; + s3FileInfos: Record; } export function getDocsWriteService(app: FdrApplication): DocsV1WriteService { - return new DocsV1WriteService({ - startDocsRegister: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: req.body.orgId, - }); - const docsRegistrationId = DocsV1Write.DocsRegistrationId(uuidv4()); - const s3FileInfos = await app.services.s3.getPresignedDocsAssetsUploadUrls({ - domain: req.body.domain, - filepaths: req.body.filepaths, - images: [], - isPrivate: true, - }); - DOCS_REGISTRATIONS[docsRegistrationId] = { - domain: req.body.domain, - orgId: req.body.orgId, - s3FileInfos, - }; - return res.send({ - docsRegistrationId, - uploadUrls: Object.fromEntries( - Object.entries(s3FileInfos).map(([filepath, fileInfo]) => { - return [filepath, fileInfo.presignedUrl]; - }), - ), - }); + return new DocsV1WriteService({ + startDocsRegister: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: req.body.orgId, + }); + const docsRegistrationId = DocsV1Write.DocsRegistrationId(uuidv4()); + const s3FileInfos = + await app.services.s3.getPresignedDocsAssetsUploadUrls({ + domain: req.body.domain, + filepaths: req.body.filepaths, + images: [], + isPrivate: true, + }); + DOCS_REGISTRATIONS[docsRegistrationId] = { + domain: req.body.domain, + orgId: req.body.orgId, + s3FileInfos, + }; + return res.send({ + docsRegistrationId, + uploadUrls: Object.fromEntries( + Object.entries(s3FileInfos).map(([filepath, fileInfo]) => { + return [filepath, fileInfo.presignedUrl]; + }) + ), + }); + }, + finishDocsRegister: async (req, res) => { + const docsRegistrationInfo = + DOCS_REGISTRATIONS[req.params.docsRegistrationId]; + if (docsRegistrationInfo == null) { + throw new DocsRegistrationIdNotFound(); + } + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: docsRegistrationInfo.orgId, + }); + const dbDocsDefinition = convertDocsDefinitionToDb({ + writeShape: req.body.docsDefinition, + files: docsRegistrationInfo.s3FileInfos, + }); + app.logger.info( + `Docs for ${docsRegistrationInfo.orgId} has references to apis ${Array.from( + dbDocsDefinition.referencedApis + ).join(", ")}` + ); + await app.services.db.prisma.docs.upsert({ + create: { + url: docsRegistrationInfo.domain, + docsDefinition: writeBuffer(dbDocsDefinition), }, - finishDocsRegister: async (req, res) => { - const docsRegistrationInfo = DOCS_REGISTRATIONS[req.params.docsRegistrationId]; - if (docsRegistrationInfo == null) { - throw new DocsRegistrationIdNotFound(); - } - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: docsRegistrationInfo.orgId, - }); - const dbDocsDefinition = convertDocsDefinitionToDb({ - writeShape: req.body.docsDefinition, - files: docsRegistrationInfo.s3FileInfos, - }); - app.logger.info( - `Docs for ${docsRegistrationInfo.orgId} has references to apis ${Array.from( - dbDocsDefinition.referencedApis, - ).join(", ")}`, - ); - await app.services.db.prisma.docs.upsert({ - create: { - url: docsRegistrationInfo.domain, - docsDefinition: writeBuffer(dbDocsDefinition), - }, - update: { - docsDefinition: writeBuffer(dbDocsDefinition), - }, - where: { - url: docsRegistrationInfo.domain, - }, - }); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete DOCS_REGISTRATIONS[req.params.docsRegistrationId]; - return res.send(); + update: { + docsDefinition: writeBuffer(dbDocsDefinition), }, - }); + where: { + url: docsRegistrationInfo.domain, + }, + }); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete DOCS_REGISTRATIONS[req.params.docsRegistrationId]; + return res.send(); + }, + }); } diff --git a/servers/fdr/src/controllers/docs/v2/getDocsReadV2Service.ts b/servers/fdr/src/controllers/docs/v2/getDocsReadV2Service.ts index 03c83ae0ea..ca5cce90b2 100644 --- a/servers/fdr/src/controllers/docs/v2/getDocsReadV2Service.ts +++ b/servers/fdr/src/controllers/docs/v2/getDocsReadV2Service.ts @@ -1,4 +1,7 @@ -import { convertDbAPIDefinitionsToRead, convertDbDocsConfigToRead } from "@fern-api/fdr-sdk"; +import { + convertDbAPIDefinitionsToRead, + convertDbDocsConfigToRead, +} from "@fern-api/fdr-sdk"; import { Cache } from "../../../Cache"; import { DocsV2Read, DocsV2ReadService } from "../../../api"; import { UserNotInOrgError } from "../../../api/generated/api"; @@ -6,147 +9,174 @@ import { DomainNotRegisteredError } from "../../../api/generated/api/resources/d import type { FdrApplication } from "../../../app"; import { ParsedBaseUrl } from "../../../util/ParsedBaseUrl"; -const DOCS_CONFIG_ID_CACHE = new Cache(100); +const DOCS_CONFIG_ID_CACHE = new Cache( + 100 +); export function getDocsReadV2Service(app: FdrApplication): DocsV2ReadService { - return new DocsV2ReadService({ - getDocsForUrl: async (req, res) => { - try { - // if the auth header belongs to fern, return the docs definition - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - } catch (e) { - // if the auth header does not belong to fern, check the org id for the docs url, and check if the user belongs to that org - if (e instanceof UserNotInOrgError) { - const parsedUrl = ParsedBaseUrl.parse(req.body.url); - const orgId = await app.dao.docsV2().getOrgIdForDocsUrl(parsedUrl.toURL()); - if (orgId == null) { - throw new DocsV2Read.DomainNotRegisteredError(); - } - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId, - }); - } - throw e; - } - const parsedUrl = ParsedBaseUrl.parse(req.body.url); - const response = await app.docsDefinitionCache.getDocsForUrl({ url: parsedUrl.toURL() }); - return res.send(response); - }, - getPrivateDocsForUrl: async (req, res) => { - // TODO: start deleting this deprecated endpoint - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - const parsedUrl = ParsedBaseUrl.parse(req.body.url); - const response = await app.docsDefinitionCache.getDocsForUrl({ url: parsedUrl.toURL() }); - return res.send(response); - }, - getOrganizationForUrl: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - const parsedUrl = ParsedBaseUrl.parse(req.body.url); - const orgId = await app.dao.docsV2().getOrgIdForDocsUrl(parsedUrl.toURL()); - if (orgId == null) { - throw new DocsV2Read.DomainNotRegisteredError(); - } - return res.send(orgId); - }, - getDocsConfigById: async (req, res) => { - try { - // if the auth header belongs to fern, return the docs definition - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - } catch (e) { - // if the auth header does not belong to fern, check the org id for the docs url, and check if the user belongs to that org - if (e instanceof UserNotInOrgError) { - const orgId = await app.dao.docsV2().getOrgIdForDocsConfigInstanceId(req.params.docsConfigId); - if (orgId == null) { - throw new DocsV2Read.DomainNotRegisteredError(); - } - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId, - }); - } - throw e; - } + return new DocsV2ReadService({ + getDocsForUrl: async (req, res) => { + try { + // if the auth header belongs to fern, return the docs definition + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + } catch (e) { + // if the auth header does not belong to fern, check the org id for the docs url, and check if the user belongs to that org + if (e instanceof UserNotInOrgError) { + const parsedUrl = ParsedBaseUrl.parse(req.body.url); + const orgId = await app.dao + .docsV2() + .getOrgIdForDocsUrl(parsedUrl.toURL()); + if (orgId == null) { + throw new DocsV2Read.DomainNotRegisteredError(); + } + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId, + }); + } + throw e; + } + const parsedUrl = ParsedBaseUrl.parse(req.body.url); + const response = await app.docsDefinitionCache.getDocsForUrl({ + url: parsedUrl.toURL(), + }); + return res.send(response); + }, + getPrivateDocsForUrl: async (req, res) => { + // TODO: start deleting this deprecated endpoint + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + const parsedUrl = ParsedBaseUrl.parse(req.body.url); + const response = await app.docsDefinitionCache.getDocsForUrl({ + url: parsedUrl.toURL(), + }); + return res.send(response); + }, + getOrganizationForUrl: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + const parsedUrl = ParsedBaseUrl.parse(req.body.url); + const orgId = await app.dao + .docsV2() + .getOrgIdForDocsUrl(parsedUrl.toURL()); + if (orgId == null) { + throw new DocsV2Read.DomainNotRegisteredError(); + } + return res.send(orgId); + }, + getDocsConfigById: async (req, res) => { + try { + // if the auth header belongs to fern, return the docs definition + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + } catch (e) { + // if the auth header does not belong to fern, check the org id for the docs url, and check if the user belongs to that org + if (e instanceof UserNotInOrgError) { + const orgId = await app.dao + .docsV2() + .getOrgIdForDocsConfigInstanceId(req.params.docsConfigId); + if (orgId == null) { + throw new DocsV2Read.DomainNotRegisteredError(); + } + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId, + }); + } + throw e; + } - let docsConfig: DocsV2Read.GetDocsConfigByIdResponse | undefined = DOCS_CONFIG_ID_CACHE.get( - req.params.docsConfigId, - ); - if (docsConfig == null) { - const loadDocsConfigResponse = await app.dao - .docsV2() - .loadDocsConfigByInstanceId(req.params.docsConfigId); - if (loadDocsConfigResponse == null) { - throw new DocsV2Read.DocsDefinitionNotFoundError(); - } - const apiDefinitions = await app.dao.apis().loadAPIDefinitions(loadDocsConfigResponse.referencedApis); - docsConfig = { - docsConfig: convertDbDocsConfigToRead({ dbShape: loadDocsConfigResponse.docsConfig }), - apis: convertDbAPIDefinitionsToRead(apiDefinitions), - }; - DOCS_CONFIG_ID_CACHE.set(req.params.docsConfigId, { - docsConfig: convertDbDocsConfigToRead({ dbShape: loadDocsConfigResponse.docsConfig }), - apis: convertDbAPIDefinitionsToRead(apiDefinitions), - }); - } - return res.send(docsConfig); - }, - // TODO: deprecate this: - getSearchApiKeyForIndexSegment: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - const { indexSegmentId } = req.body; - const cachedKey = app.services.algoliaIndexSegmentManager.getSearchApiKeyForIndexSegment(indexSegmentId); - if (cachedKey != null) { - return res.send({ searchApiKey: cachedKey }); - } - const indexSegment = await app.dao.indexSegment().loadIndexSegment(indexSegmentId); - if (indexSegment == null) { - throw new DocsV2Read.IndexSegmentNotFoundError(); - } - const searchApiKey = app.services.algoliaIndexSegmentManager.generateAndCacheApiKey(indexSegmentId); - return res.send({ searchApiKey }); - }, - listAllDocsUrls: async (req, res) => { - // must be a fern employee - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); + let docsConfig: DocsV2Read.GetDocsConfigByIdResponse | undefined = + DOCS_CONFIG_ID_CACHE.get(req.params.docsConfigId); + if (docsConfig == null) { + const loadDocsConfigResponse = await app.dao + .docsV2() + .loadDocsConfigByInstanceId(req.params.docsConfigId); + if (loadDocsConfigResponse == null) { + throw new DocsV2Read.DocsDefinitionNotFoundError(); + } + const apiDefinitions = await app.dao + .apis() + .loadAPIDefinitions(loadDocsConfigResponse.referencedApis); + docsConfig = { + docsConfig: convertDbDocsConfigToRead({ + dbShape: loadDocsConfigResponse.docsConfig, + }), + apis: convertDbAPIDefinitionsToRead(apiDefinitions), + }; + DOCS_CONFIG_ID_CACHE.set(req.params.docsConfigId, { + docsConfig: convertDbDocsConfigToRead({ + dbShape: loadDocsConfigResponse.docsConfig, + }), + apis: convertDbAPIDefinitionsToRead(apiDefinitions), + }); + } + return res.send(docsConfig); + }, + // TODO: deprecate this: + getSearchApiKeyForIndexSegment: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + const { indexSegmentId } = req.body; + const cachedKey = + app.services.algoliaIndexSegmentManager.getSearchApiKeyForIndexSegment( + indexSegmentId + ); + if (cachedKey != null) { + return res.send({ searchApiKey: cachedKey }); + } + const indexSegment = await app.dao + .indexSegment() + .loadIndexSegment(indexSegmentId); + if (indexSegment == null) { + throw new DocsV2Read.IndexSegmentNotFoundError(); + } + const searchApiKey = + app.services.algoliaIndexSegmentManager.generateAndCacheApiKey( + indexSegmentId + ); + return res.send({ searchApiKey }); + }, + listAllDocsUrls: async (req, res) => { + // must be a fern employee + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); - return res.send( - await app.dao.docsV2().listAllDocsUrls({ - limit: req.query.limit, - page: req.query.page, - customOnly: req.query.custom, - domainSuffix: app.config.domainSuffix, - }), - ); - }, - getDocsUrlMetadata: async (req, res) => { - const parsedUrl = ParsedBaseUrl.parse(req.body.url); - const metadata = await app.dao.docsV2().loadDocsMetadata(parsedUrl.toURL()); - if (metadata != null) { - return res.send({ - isPreviewUrl: metadata.isPreview, - org: metadata.orgId, - url: req.body.url, - }); - } - throw new DomainNotRegisteredError(); - }, - }); + return res.send( + await app.dao.docsV2().listAllDocsUrls({ + limit: req.query.limit, + page: req.query.page, + customOnly: req.query.custom, + domainSuffix: app.config.domainSuffix, + }) + ); + }, + getDocsUrlMetadata: async (req, res) => { + const parsedUrl = ParsedBaseUrl.parse(req.body.url); + const metadata = await app.dao + .docsV2() + .loadDocsMetadata(parsedUrl.toURL()); + if (metadata != null) { + return res.send({ + isPreviewUrl: metadata.isPreview, + org: metadata.orgId, + url: req.body.url, + }); + } + throw new DomainNotRegisteredError(); + }, + }); } diff --git a/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts b/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts index 5856da8039..5f535fc0de 100644 --- a/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts +++ b/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts @@ -1,12 +1,12 @@ import { - APIV1Db, - convertDbAPIDefinitionToRead, - convertDocsDefinitionToDb, - convertDocsDefinitionToRead, - DocsV1Db, - DocsV1Write, - FdrAPI, - FernNavigation, + APIV1Db, + convertDbAPIDefinitionToRead, + convertDocsDefinitionToDb, + convertDocsDefinitionToRead, + DocsV1Db, + DocsV1Write, + FdrAPI, + FernNavigation, } from "@fern-api/fdr-sdk"; import { isNonNullish } from "@fern-api/ui-core-utils"; import { generateAlgoliaRecords } from "@fern-docs/search-server/archive"; @@ -17,13 +17,16 @@ import { v4 as uuidv4 } from "uuid"; import { DocsV2WriteService } from "../../../api"; import { FernRegistry } from "../../../api/generated"; import { OrgId } from "../../../api/generated/api"; -import { DomainBelongsToAnotherOrgError, InvalidUrlError } from "../../../api/generated/api/resources/commons/errors"; +import { + DomainBelongsToAnotherOrgError, + InvalidUrlError, +} from "../../../api/generated/api/resources/commons/errors"; import { DocsRegistrationIdNotFound } from "../../../api/generated/api/resources/docs/resources/v1/resources/write/errors"; import { LoadDocsForUrlResponse } from "../../../api/generated/api/resources/docs/resources/v2/resources/read"; import { - DocsNotFoundError, - InvalidDomainError, - ReindexNotAllowedError, + DocsNotFoundError, + InvalidDomainError, + ReindexNotAllowedError, } from "../../../api/generated/api/resources/docs/resources/v2/resources/write/errors"; import { type FdrApplication } from "../../../app"; import { AlgoliaSearchRecord, IndexSegment } from "../../../services/algolia"; @@ -33,416 +36,494 @@ import { ParsedBaseUrl } from "../../../util/ParsedBaseUrl"; import { getSearchInfoFromDocs } from "../v1/getDocsReadService"; export interface DocsRegistrationInfo { - fernUrl: ParsedBaseUrl; - customUrls: ParsedBaseUrl[]; - orgId: FdrAPI.OrgId; - s3FileInfos: Record; - isPreview: boolean; - authType: AuthType; + fernUrl: ParsedBaseUrl; + customUrls: ParsedBaseUrl[]; + orgId: FdrAPI.OrgId; + s3FileInfos: Record; + isPreview: boolean; + authType: AuthType; } function pathnameIsMalformed(pathname: string): boolean { - if (pathname === "" || pathname === "/") { - return false; - } - if (!/^.*([a-z0-9]).*$/.test(pathname)) { - // does the pathname only contain special characters? - return true; - } + if (pathname === "" || pathname === "/") { return false; + } + if (!/^.*([a-z0-9]).*$/.test(pathname)) { + // does the pathname only contain special characters? + return true; + } + return false; } -function validateAndParseFernDomainUrl({ app, url }: { app: FdrApplication; url: string }): ParsedBaseUrl { - const baseUrl = ParsedBaseUrl.parse(url); - if (baseUrl.path != null && pathnameIsMalformed(baseUrl.path)) { - throw new InvalidUrlError(`Domain URL is malformed: https://${baseUrl.hostname + baseUrl.path}`); - } - if (!baseUrl.hostname.endsWith(app.config.domainSuffix)) { - throw new InvalidDomainError(); - } - return baseUrl; +function validateAndParseFernDomainUrl({ + app, + url, +}: { + app: FdrApplication; + url: string; +}): ParsedBaseUrl { + const baseUrl = ParsedBaseUrl.parse(url); + if (baseUrl.path != null && pathnameIsMalformed(baseUrl.path)) { + throw new InvalidUrlError( + `Domain URL is malformed: https://${baseUrl.hostname + baseUrl.path}` + ); + } + if (!baseUrl.hostname.endsWith(app.config.domainSuffix)) { + throw new InvalidDomainError(); + } + return baseUrl; } -function parseCustomDomainUrls({ customUrls }: { customUrls: string[] }): ParsedBaseUrl[] { - const parsedUrls: ParsedBaseUrl[] = []; - for (const customUrl of customUrls) { - const baseUrl = ParsedBaseUrl.parse(customUrl); - parsedUrls.push(baseUrl); - } - return parsedUrls; +function parseCustomDomainUrls({ + customUrls, +}: { + customUrls: string[]; +}): ParsedBaseUrl[] { + const parsedUrls: ParsedBaseUrl[] = []; + for (const customUrl of customUrls) { + const baseUrl = ParsedBaseUrl.parse(customUrl); + parsedUrls.push(baseUrl); + } + return parsedUrls; } export function getDocsWriteV2Service(app: FdrApplication): DocsV2WriteService { - return new DocsV2WriteService({ - startDocsRegister: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: req.body.orgId, - }); - - const fernUrl = validateAndParseFernDomainUrl({ app, url: req.body.domain }); - const customUrls = parseCustomDomainUrls({ customUrls: req.body.customDomains }); - - // ensure that the domains are not already registered by another org - const { allDomainsOwned: hasOwnership, unownedDomains } = await app.dao - .docsV2() - .checkDomainsDontBelongToAnotherOrg( - [fernUrl, ...customUrls].map((url) => url.getFullUrl()), - req.body.orgId, - ); - if (!hasOwnership) { - throw new DomainBelongsToAnotherOrgError( - `The following domains belong to another organization: ${unownedDomains.join(", ")}`, - ); - } - - const docsRegistrationId = DocsV1Write.DocsRegistrationId(uuidv4()); - const s3FileInfos = await app.services.s3.getPresignedDocsAssetsUploadUrls({ - domain: req.body.domain, - filepaths: req.body.filepaths, - images: req.body.images ?? [], - isPrivate: req.body.authConfig?.type === "private", - }); - - await app.services.slack.notifyGeneratedDocs({ - orgId: req.body.orgId, - urls: [fernUrl.toURL().toString(), ...customUrls.map((url) => url.toURL().toString())], - }); - await app.dao.docsRegistration().storeDocsRegistrationById(docsRegistrationId, { - fernUrl, - customUrls, - orgId: req.body.orgId, - s3FileInfos, - isPreview: false, - authType: req.body.authConfig?.type === "private" ? AuthType.WORKOS_SSO : AuthType.PUBLIC, - }); - return res.send({ - docsRegistrationId, - uploadUrls: Object.fromEntries( - Object.entries(s3FileInfos).map(([filepath, fileInfo]) => { - return [filepath, fileInfo.presignedUrl]; - }), - ), - }); - }, - startDocsPreviewRegister: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: req.body.orgId, - }); - const docsRegistrationId = DocsV1Write.DocsRegistrationId(uuidv4()); - const fernUrl = ParsedBaseUrl.parse( - urlJoin( - `${req.body.orgId}-preview-${docsRegistrationId}.${app.config.domainSuffix}`, - req.body.basePath ?? "", - ), - ); - const s3FileInfos = await app.services.s3.getPresignedDocsAssetsUploadUrls({ - domain: fernUrl.hostname, - filepaths: req.body.filepaths, - images: req.body.images ?? [], - isPrivate: req.body.authConfig?.type === "private", - }); - await app.dao.docsRegistration().storeDocsRegistrationById(docsRegistrationId, { - fernUrl, - customUrls: [], - orgId: req.body.orgId, - s3FileInfos, - isPreview: true, - authType: req.body.authConfig?.type === "private" ? AuthType.WORKOS_SSO : AuthType.PUBLIC, - }); - return res.send({ - docsRegistrationId, - uploadUrls: Object.fromEntries( - Object.entries(s3FileInfos).map(([filepath, fileInfo]) => { - return [filepath, fileInfo.presignedUrl]; - }), - ), - previewUrl: `https://${fernUrl.getFullUrl()}`, - }); - }, - finishDocsRegister: async (req, res) => { - const docsRegistrationInfo = await app.dao - .docsRegistration() - .getDocsRegistrationById(req.params.docsRegistrationId); - if (docsRegistrationInfo == null) { - throw new DocsRegistrationIdNotFound(); - } - try { - app.logger.debug(`[${docsRegistrationInfo.fernUrl.getFullUrl()}] Called finishDocsRegister`); - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: docsRegistrationInfo.orgId, - }); - - app.logger.debug(`[${docsRegistrationInfo.fernUrl.getFullUrl()}] Transforming Docs Definition to DB`); - const dbDocsDefinition = convertDocsDefinitionToDb({ - writeShape: req.body.docsDefinition, - files: docsRegistrationInfo.s3FileInfos, - }); - - const apiDefinitions = ( - await Promise.all( - dbDocsDefinition.referencedApis.map(async (id) => await app.services.db.getApiDefinition(id)), - ) - ).filter(isNonNullish); - const apiDefinitionsById = Object.fromEntries( - apiDefinitions.map((definition) => [definition.id, definition]), - ); - - const warmEndpointCachePromises = apiDefinitions.flatMap((apiDefinition) => { - return Object.entries(apiDefinition.subpackages).flatMap(([id, subpackage]) => { - return subpackage.endpoints.map(async (endpoint) => { - try { - return await fetch( - `https://${docsRegistrationInfo.fernUrl.getFullUrl()}/api/fern-docs/api-definition/${apiDefinition.id}/endpoint/${endpoint.originalEndpointId}`, - ); - } catch (e: Error | unknown) { - app.logger.error( - `Error while trying to warm endpoint cache for ${JSON.stringify(docsRegistrationInfo.fernUrl)} ${e instanceof Error ? e.stack : ""}`, - ); - throw e; - } - }); - }); - }); - - const indexSegments = await uploadToAlgoliaForRegistration( - app, - docsRegistrationInfo, - dbDocsDefinition, - apiDefinitionsById, - ); + return new DocsV2WriteService({ + startDocsRegister: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: req.body.orgId, + }); + + const fernUrl = validateAndParseFernDomainUrl({ + app, + url: req.body.domain, + }); + const customUrls = parseCustomDomainUrls({ + customUrls: req.body.customDomains, + }); + + // ensure that the domains are not already registered by another org + const { allDomainsOwned: hasOwnership, unownedDomains } = await app.dao + .docsV2() + .checkDomainsDontBelongToAnotherOrg( + [fernUrl, ...customUrls].map((url) => url.getFullUrl()), + req.body.orgId + ); + if (!hasOwnership) { + throw new DomainBelongsToAnotherOrgError( + `The following domains belong to another organization: ${unownedDomains.join(", ")}` + ); + } + + const docsRegistrationId = DocsV1Write.DocsRegistrationId(uuidv4()); + const s3FileInfos = + await app.services.s3.getPresignedDocsAssetsUploadUrls({ + domain: req.body.domain, + filepaths: req.body.filepaths, + images: req.body.images ?? [], + isPrivate: req.body.authConfig?.type === "private", + }); + + await app.services.slack.notifyGeneratedDocs({ + orgId: req.body.orgId, + urls: [ + fernUrl.toURL().toString(), + ...customUrls.map((url) => url.toURL().toString()), + ], + }); + await app.dao + .docsRegistration() + .storeDocsRegistrationById(docsRegistrationId, { + fernUrl, + customUrls, + orgId: req.body.orgId, + s3FileInfos, + isPreview: false, + authType: + req.body.authConfig?.type === "private" + ? AuthType.WORKOS_SSO + : AuthType.PUBLIC, + }); + return res.send({ + docsRegistrationId, + uploadUrls: Object.fromEntries( + Object.entries(s3FileInfos).map(([filepath, fileInfo]) => { + return [filepath, fileInfo.presignedUrl]; + }) + ), + }); + }, + startDocsPreviewRegister: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: req.body.orgId, + }); + const docsRegistrationId = DocsV1Write.DocsRegistrationId(uuidv4()); + const fernUrl = ParsedBaseUrl.parse( + urlJoin( + `${req.body.orgId}-preview-${docsRegistrationId}.${app.config.domainSuffix}`, + req.body.basePath ?? "" + ) + ); + const s3FileInfos = + await app.services.s3.getPresignedDocsAssetsUploadUrls({ + domain: fernUrl.hostname, + filepaths: req.body.filepaths, + images: req.body.images ?? [], + isPrivate: req.body.authConfig?.type === "private", + }); + await app.dao + .docsRegistration() + .storeDocsRegistrationById(docsRegistrationId, { + fernUrl, + customUrls: [], + orgId: req.body.orgId, + s3FileInfos, + isPreview: true, + authType: + req.body.authConfig?.type === "private" + ? AuthType.WORKOS_SSO + : AuthType.PUBLIC, + }); + return res.send({ + docsRegistrationId, + uploadUrls: Object.fromEntries( + Object.entries(s3FileInfos).map(([filepath, fileInfo]) => { + return [filepath, fileInfo.presignedUrl]; + }) + ), + previewUrl: `https://${fernUrl.getFullUrl()}`, + }); + }, + finishDocsRegister: async (req, res) => { + const docsRegistrationInfo = await app.dao + .docsRegistration() + .getDocsRegistrationById(req.params.docsRegistrationId); + if (docsRegistrationInfo == null) { + throw new DocsRegistrationIdNotFound(); + } + try { + app.logger.debug( + `[${docsRegistrationInfo.fernUrl.getFullUrl()}] Called finishDocsRegister` + ); + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: docsRegistrationInfo.orgId, + }); - await app.docsDefinitionCache.storeDocsForUrl({ - docsRegistrationInfo, - dbDocsDefinition, - indexSegments, - }); + app.logger.debug( + `[${docsRegistrationInfo.fernUrl.getFullUrl()}] Transforming Docs Definition to DB` + ); + const dbDocsDefinition = convertDocsDefinitionToDb({ + writeShape: req.body.docsDefinition, + files: docsRegistrationInfo.s3FileInfos, + }); + + const apiDefinitions = ( + await Promise.all( + dbDocsDefinition.referencedApis.map( + async (id) => await app.services.db.getApiDefinition(id) + ) + ) + ).filter(isNonNullish); + const apiDefinitionsById = Object.fromEntries( + apiDefinitions.map((definition) => [definition.id, definition]) + ); - /** - * IMPORTANT NOTE: - * vercel cache is not shared between custom domains, so we need to revalidate on EACH custom domain individually - */ - const urls = [docsRegistrationInfo.fernUrl, ...docsRegistrationInfo.customUrls]; - - try { - await Promise.all( - urls.map(async (baseUrl) => { - const results = await app.services.revalidator.revalidate({ baseUrl, app }); - if (results.failed.length === 0 && !results.revalidationFailed) { - app.logger.info(`Successfully revalidated ${results.successful.length} paths.`); - } else { - await app.services.slack.notifyFailedToRevalidatePaths({ - domain: baseUrl.getFullUrl(), - paths: results, - }); - } - }), + const warmEndpointCachePromises = apiDefinitions.flatMap( + (apiDefinition) => { + return Object.entries(apiDefinition.subpackages).flatMap( + ([id, subpackage]) => { + return subpackage.endpoints.map(async (endpoint) => { + try { + return await fetch( + `https://${docsRegistrationInfo.fernUrl.getFullUrl()}/api/fern-docs/api-definition/${apiDefinition.id}/endpoint/${endpoint.originalEndpointId}` + ); + } catch (e: Error | unknown) { + app.logger.error( + `Error while trying to warm endpoint cache for ${JSON.stringify(docsRegistrationInfo.fernUrl)} ${e instanceof Error ? e.stack : ""}` ); - } catch (e) { - app.logger.error(`Error while trying to revalidate docs for ${docsRegistrationInfo.fernUrl}`, e); - await app.services.slack.notifyFailedToRegisterDocs({ - domain: docsRegistrationInfo.fernUrl.getFullUrl(), - err: e, - }); throw e; - } - - await Promise.all(warmEndpointCachePromises); - - return await res.send(); - } catch (e) { - app.logger.error(`Error while trying to register docs for ${docsRegistrationInfo.fernUrl}`, e); - await app.services.slack.notifyFailedToRegisterDocs({ - domain: docsRegistrationInfo.fernUrl.getFullUrl(), - err: e, + } }); - throw e; - } - }, - reindexAlgoliaSearchRecords: async (req, res) => { - // step 1. load from db - const parsedUrl = ParsedBaseUrl.parse(req.body.url); - const response = await app.dao.docsV2().loadDocsForURL(parsedUrl.toURL()); - - if (response == null) { - throw new DocsNotFoundError(); - } - - if (response.authType !== AuthType.PUBLIC || response.isPreview || response.docsConfigInstanceId == null) { - throw new ReindexNotAllowedError(); - } - - const apiDefinitions = ( - await Promise.all( - response.docsDefinition.referencedApis.map( - async (id) => await app.services.db.getApiDefinition(id), - ), - ) - ).filter(isNonNullish); - const apiDefinitionsById = Object.fromEntries( - apiDefinitions.map((definition) => [definition.id, definition]), + } ); + } + ); + + const indexSegments = await uploadToAlgoliaForRegistration( + app, + docsRegistrationInfo, + dbDocsDefinition, + apiDefinitionsById + ); + + await app.docsDefinitionCache.storeDocsForUrl({ + docsRegistrationInfo, + dbDocsDefinition, + indexSegments, + }); + + /** + * IMPORTANT NOTE: + * vercel cache is not shared between custom domains, so we need to revalidate on EACH custom domain individually + */ + const urls = [ + docsRegistrationInfo.fernUrl, + ...docsRegistrationInfo.customUrls, + ]; - // step 2. create new index segments in algolia - const indexSegments = await uploadToAlgolia( + try { + await Promise.all( + urls.map(async (baseUrl) => { + const results = await app.services.revalidator.revalidate({ + baseUrl, app, - ParsedBaseUrl.parse(response.domain), - response.docsDefinition, - apiDefinitionsById, - response.algoliaIndex, - response.docsConfigInstanceId, - ); + }); + if (results.failed.length === 0 && !results.revalidationFailed) { + app.logger.info( + `Successfully revalidated ${results.successful.length} paths.` + ); + } else { + await app.services.slack.notifyFailedToRevalidatePaths({ + domain: baseUrl.getFullUrl(), + paths: results, + }); + } + }) + ); + } catch (e) { + app.logger.error( + `Error while trying to revalidate docs for ${docsRegistrationInfo.fernUrl}`, + e + ); + await app.services.slack.notifyFailedToRegisterDocs({ + domain: docsRegistrationInfo.fernUrl.getFullUrl(), + err: e, + }); + throw e; + } - // step 3. store docs + new algolia segments - await app.docsDefinitionCache.replaceDocsForInstanceId({ - instanceId: response.docsConfigInstanceId, - dbDocsDefinition: response.docsDefinition, - indexSegments, - }); - - return await res.send(); - }, - transferOwnershipOfDomain: async (req, res) => { - // only fern users can transfer domain ownership - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); - - const parsedUrl = ParsedBaseUrl.parse(req.body.domain); - - await app.dao.docsV2().transferDomainOwner({ - domain: parsedUrl.getFullUrl(), - toOrgId: req.body.toOrgId, - }); - - return await res.send(); - }, - }); + await Promise.all(warmEndpointCachePromises); + + return await res.send(); + } catch (e) { + app.logger.error( + `Error while trying to register docs for ${docsRegistrationInfo.fernUrl}`, + e + ); + await app.services.slack.notifyFailedToRegisterDocs({ + domain: docsRegistrationInfo.fernUrl.getFullUrl(), + err: e, + }); + throw e; + } + }, + reindexAlgoliaSearchRecords: async (req, res) => { + // step 1. load from db + const parsedUrl = ParsedBaseUrl.parse(req.body.url); + const response = await app.dao.docsV2().loadDocsForURL(parsedUrl.toURL()); + + if (response == null) { + throw new DocsNotFoundError(); + } + + if ( + response.authType !== AuthType.PUBLIC || + response.isPreview || + response.docsConfigInstanceId == null + ) { + throw new ReindexNotAllowedError(); + } + + const apiDefinitions = ( + await Promise.all( + response.docsDefinition.referencedApis.map( + async (id) => await app.services.db.getApiDefinition(id) + ) + ) + ).filter(isNonNullish); + const apiDefinitionsById = Object.fromEntries( + apiDefinitions.map((definition) => [definition.id, definition]) + ); + + // step 2. create new index segments in algolia + const indexSegments = await uploadToAlgolia( + app, + ParsedBaseUrl.parse(response.domain), + response.docsDefinition, + apiDefinitionsById, + response.algoliaIndex, + response.docsConfigInstanceId + ); + + // step 3. store docs + new algolia segments + await app.docsDefinitionCache.replaceDocsForInstanceId({ + instanceId: response.docsConfigInstanceId, + dbDocsDefinition: response.docsDefinition, + indexSegments, + }); + + return await res.send(); + }, + transferOwnershipOfDomain: async (req, res) => { + // only fern users can transfer domain ownership + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); + + const parsedUrl = ParsedBaseUrl.parse(req.body.domain); + + await app.dao.docsV2().transferDomainOwner({ + domain: parsedUrl.getFullUrl(), + toOrgId: req.body.toOrgId, + }); + + return await res.send(); + }, + }); } async function uploadToAlgoliaForRegistration( - app: FdrApplication, - docsRegistrationInfo: DocsRegistrationInfo, - dbDocsDefinition: WithoutQuestionMarks, - apiDefinitionsById: Record, + app: FdrApplication, + docsRegistrationInfo: DocsRegistrationInfo, + dbDocsDefinition: WithoutQuestionMarks, + apiDefinitionsById: Record ): Promise { - // TODO: make sure to store private docs index into user-restricted algolia index - // see https://www.algolia.com/doc/guides/security/api-keys/how-to/user-restricted-access-to-data/ - if (docsRegistrationInfo.authType !== AuthType.PUBLIC) { - return []; - } - - // skip algolia step for preview - if (docsRegistrationInfo.isPreview) { - return []; - } - - return uploadToAlgolia(app, docsRegistrationInfo.fernUrl, dbDocsDefinition, apiDefinitionsById); + // TODO: make sure to store private docs index into user-restricted algolia index + // see https://www.algolia.com/doc/guides/security/api-keys/how-to/user-restricted-access-to-data/ + if (docsRegistrationInfo.authType !== AuthType.PUBLIC) { + return []; + } + + // skip algolia step for preview + if (docsRegistrationInfo.isPreview) { + return []; + } + + return uploadToAlgolia( + app, + docsRegistrationInfo.fernUrl, + dbDocsDefinition, + apiDefinitionsById + ); } async function uploadToAlgolia( - app: FdrApplication, - url: ParsedBaseUrl, - dbDocsDefinition: WithoutQuestionMarks, - apiDefinitionsById: Record, - algoliaIndex?: FernRegistry.AlgoliaSearchIndex, - docsConfigInstanceId?: DocsV1Write.DocsConfigId, + app: FdrApplication, + url: ParsedBaseUrl, + dbDocsDefinition: WithoutQuestionMarks, + apiDefinitionsById: Record, + algoliaIndex?: FernRegistry.AlgoliaSearchIndex, + docsConfigInstanceId?: DocsV1Write.DocsConfigId ): Promise { - app.logger.debug(`[${url.getFullUrl()}] Generating new index segments`); - const generateNewIndexSegmentsResult = app.services.algoliaIndexSegmentManager.generateIndexSegmentsForDefinition({ - dbDocsDefinition, - url: url.getFullUrl(), + app.logger.debug(`[${url.getFullUrl()}] Generating new index segments`); + const generateNewIndexSegmentsResult = + app.services.algoliaIndexSegmentManager.generateIndexSegmentsForDefinition({ + dbDocsDefinition, + url: url.getFullUrl(), }); - const configSegmentTuples = - generateNewIndexSegmentsResult.type === "versioned" - ? generateNewIndexSegmentsResult.configSegmentTuples - : [generateNewIndexSegmentsResult.configSegmentTuple]; - const newIndexSegments = configSegmentTuples.map(([, seg]) => seg); - - app.logger.debug(`[${url.getFullUrl()}] Generating search records for all versions`); - - let searchRecords: AlgoliaSearchRecord[] = []; - if (dbDocsDefinition.config.root == null) { + const configSegmentTuples = + generateNewIndexSegmentsResult.type === "versioned" + ? generateNewIndexSegmentsResult.configSegmentTuples + : [generateNewIndexSegmentsResult.configSegmentTuple]; + const newIndexSegments = configSegmentTuples.map(([, seg]) => seg); + + app.logger.debug( + `[${url.getFullUrl()}] Generating search records for all versions` + ); + + let searchRecords: AlgoliaSearchRecord[] = []; + if (dbDocsDefinition.config.root == null) { + try { + searchRecords = await app.services.algolia.generateSearchRecords({ + url: url.getFullUrl(), + docsDefinition: dbDocsDefinition, + apiDefinitionsById, + configSegmentTuples, + }); + } catch (e) { + app.logger.error( + `Error while trying to generate search records for ${url.getFullUrl()}`, + e + ); + await app.services.slack.notify( + `Fatal error thrown while generating search records for ${url.getFullUrl()}. Search may not be available for this docs instance.`, + e + ); + throw e; + } + } else { + const loadDocsForUrlResponse: LoadDocsForUrlResponse = { + baseUrl: { + domain: url.hostname, + basePath: url.path?.trim(), + }, + definition: convertDocsDefinitionToRead({ + docsDbDefinition: dbDocsDefinition, + algoliaSearchIndex: algoliaIndex, + // we don't need to use this for generating algolia records + filesV2: {}, + apis: mapValues(apiDefinitionsById, (def) => + convertDbAPIDefinitionToRead(def) + ), + id: docsConfigInstanceId ?? DocsV1Write.DocsConfigId(""), + search: getSearchInfoFromDocs({ + algoliaIndex, + indexSegmentIds: newIndexSegments.map( + (indexSegment) => indexSegment.id + ), + activeIndexSegments: newIndexSegments.map((indexSegment) => ({ + id: indexSegment.id, + createdAt: new Date(), + version: null, + })), + docsDbDefinition: dbDocsDefinition, + app, + }), + }), + lightModeEnabled: dbDocsDefinition.config.colorsV3?.type !== "dark", + orgId: OrgId("dummy"), + }; + await Promise.all( + configSegmentTuples.map(async ([_, indexSegment]) => { try { - searchRecords = await app.services.algolia.generateSearchRecords({ - url: url.getFullUrl(), - docsDefinition: dbDocsDefinition, - apiDefinitionsById, - configSegmentTuples, - }); + const v2Records = generateAlgoliaRecords({ + indexSegmentId: indexSegment.id, + nodes: FernNavigation.utils.toRootNode(loadDocsForUrlResponse), + pages: FernNavigation.utils.toPages(loadDocsForUrlResponse), + apis: FernNavigation.utils.toApis(loadDocsForUrlResponse), + isFieldRecordsEnabled: true, + }); + searchRecords.push( + ...v2Records.map((record) => ({ + ...record, + objectID: uuidv4(), + })) + ); } catch (e) { - app.logger.error(`Error while trying to generate search records for ${url.getFullUrl()}`, e); - await app.services.slack.notify( - `Fatal error thrown while generating search records for ${url.getFullUrl()}. Search may not be available for this docs instance.`, - e, - ); - throw e; + app.logger.error( + `Error while trying to generate search records for ${url.getFullUrl()}`, + e + ); + await app.services.slack.notify( + `Fatal error thrown while generating search records for ${url.getFullUrl()}. Search may not be available for this docs instance.`, + e + ); + throw e; } - } else { - const loadDocsForUrlResponse: LoadDocsForUrlResponse = { - baseUrl: { - domain: url.hostname, - basePath: url.path?.trim(), - }, - definition: convertDocsDefinitionToRead({ - docsDbDefinition: dbDocsDefinition, - algoliaSearchIndex: algoliaIndex, - // we don't need to use this for generating algolia records - filesV2: {}, - apis: mapValues(apiDefinitionsById, (def) => convertDbAPIDefinitionToRead(def)), - id: docsConfigInstanceId ?? DocsV1Write.DocsConfigId(""), - search: getSearchInfoFromDocs({ - algoliaIndex, - indexSegmentIds: newIndexSegments.map((indexSegment) => indexSegment.id), - activeIndexSegments: newIndexSegments.map((indexSegment) => ({ - id: indexSegment.id, - createdAt: new Date(), - version: null, - })), - docsDbDefinition: dbDocsDefinition, - app, - }), - }), - lightModeEnabled: dbDocsDefinition.config.colorsV3?.type !== "dark", - orgId: OrgId("dummy"), - }; - await Promise.all( - configSegmentTuples.map(async ([_, indexSegment]) => { - try { - const v2Records = generateAlgoliaRecords({ - indexSegmentId: indexSegment.id, - nodes: FernNavigation.utils.toRootNode(loadDocsForUrlResponse), - pages: FernNavigation.utils.toPages(loadDocsForUrlResponse), - apis: FernNavigation.utils.toApis(loadDocsForUrlResponse), - isFieldRecordsEnabled: true, - }); - searchRecords.push( - ...v2Records.map((record) => ({ - ...record, - objectID: uuidv4(), - })), - ); - } catch (e) { - app.logger.error(`Error while trying to generate search records for ${url.getFullUrl()}`, e); - await app.services.slack.notify( - `Fatal error thrown while generating search records for ${url.getFullUrl()}. Search may not be available for this docs instance.`, - e, - ); - throw e; - } - }), - ); - } + }) + ); + } - app.logger.debug(`[${url.getFullUrl()}] Uploading search records to Algolia`); - await app.services.algolia.uploadSearchRecords(searchRecords); + app.logger.debug(`[${url.getFullUrl()}] Uploading search records to Algolia`); + await app.services.algolia.uploadSearchRecords(searchRecords); - app.logger.debug(`[${url.getFullUrl()}] Updating db docs definitions`); + app.logger.debug(`[${url.getFullUrl()}] Updating db docs definitions`); - return newIndexSegments; + return newIndexSegments; } /** @@ -451,9 +532,14 @@ async function uploadToAlgolia( * @returns staging URL or undefined if the URL is not a production URL */ function createStagingUrl(url: ParsedBaseUrl): ParsedBaseUrl | undefined { - const maybeProdUrl = url.getFullUrl(); - if (maybeProdUrl.includes(".docs.buildwithfern.com")) { - return ParsedBaseUrl.parse(maybeProdUrl.replace(".docs.buildwithfern.com", ".docs.staging.buildwithfern.com")); - } - return undefined; + const maybeProdUrl = url.getFullUrl(); + if (maybeProdUrl.includes(".docs.buildwithfern.com")) { + return ParsedBaseUrl.parse( + maybeProdUrl.replace( + ".docs.buildwithfern.com", + ".docs.staging.buildwithfern.com" + ) + ); + } + return undefined; } diff --git a/servers/fdr/src/controllers/generators/getGeneratorsCliController.ts b/servers/fdr/src/controllers/generators/getGeneratorsCliController.ts index 84e2e46a3c..b4d8145586 100644 --- a/servers/fdr/src/controllers/generators/getGeneratorsCliController.ts +++ b/servers/fdr/src/controllers/generators/getGeneratorsCliController.ts @@ -1,59 +1,67 @@ import { - CliVersionNotFoundError, - NoValidCliForIrError, - NoValidClisFoundError, + CliVersionNotFoundError, + NoValidCliForIrError, + NoValidClisFoundError, } from "../../api/generated/api/resources/generators"; import { CliService } from "../../api/generated/api/resources/generators/resources/cli/service/CliService"; import { FdrApplication } from "../../app"; export function getGeneratorsCliController(app: FdrApplication): CliService { - return new CliService({ - getLatestCliRelease: async (req, res) => { - const maybeLatestRelease = await app.dao.cliVersions().getLatestCliRelease({ - getLatestCliReleaseRequest: req.body, - }); - if (!maybeLatestRelease) { - throw new NoValidClisFoundError(); - } - return res.send(maybeLatestRelease); - }, - getChangelog: async (req, res) => { - return res.send( - await app.dao.cliVersions().getChangelog({ - versionRanges: req.body, - }), - ); - }, - getMinCliForIr: async (req, res) => { - const irVersion = Number(req.params.irVersion); - const maybeRelease = await app.dao.cliVersions().getMinCliForIr({ irVersion }); - if (!maybeRelease) { - throw new NoValidCliForIrError({ providedVersion: irVersion }); - } - return res.send(maybeRelease); - }, - upsertCliRelease: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); + return new CliService({ + getLatestCliRelease: async (req, res) => { + const maybeLatestRelease = await app.dao + .cliVersions() + .getLatestCliRelease({ + getLatestCliReleaseRequest: req.body, + }); + if (!maybeLatestRelease) { + throw new NoValidClisFoundError(); + } + return res.send(maybeLatestRelease); + }, + getChangelog: async (req, res) => { + return res.send( + await app.dao.cliVersions().getChangelog({ + versionRanges: req.body, + }) + ); + }, + getMinCliForIr: async (req, res) => { + const irVersion = Number(req.params.irVersion); + const maybeRelease = await app.dao + .cliVersions() + .getMinCliForIr({ irVersion }); + if (!maybeRelease) { + throw new NoValidCliForIrError({ providedVersion: irVersion }); + } + return res.send(maybeRelease); + }, + upsertCliRelease: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); - await app.dao.cliVersions().upsertCliRelease({ cliRelease: req.body }); - }, - getCliRelease: async (req, res) => { - const maybeRelease = await app.dao.cliVersions().getCliRelease({ cliVersion: req.params.cliVersion }); - if (!maybeRelease) { - throw new CliVersionNotFoundError({ providedVersion: req.params.cliVersion }); - } - return res.send(maybeRelease); - }, - listCliReleases: async (req, res) => { - return res.send( - await app.dao.cliVersions().listCliReleases({ - page: req.query.page, - pageSize: req.query.pageSize, - }), - ); - }, - }); + await app.dao.cliVersions().upsertCliRelease({ cliRelease: req.body }); + }, + getCliRelease: async (req, res) => { + const maybeRelease = await app.dao + .cliVersions() + .getCliRelease({ cliVersion: req.params.cliVersion }); + if (!maybeRelease) { + throw new CliVersionNotFoundError({ + providedVersion: req.params.cliVersion, + }); + } + return res.send(maybeRelease); + }, + listCliReleases: async (req, res) => { + return res.send( + await app.dao.cliVersions().listCliReleases({ + page: req.query.page, + pageSize: req.query.pageSize, + }) + ); + }, + }); } diff --git a/servers/fdr/src/controllers/generators/getGeneratorsRootController.ts b/servers/fdr/src/controllers/generators/getGeneratorsRootController.ts index c8f2666cb3..2b210288c1 100644 --- a/servers/fdr/src/controllers/generators/getGeneratorsRootController.ts +++ b/servers/fdr/src/controllers/generators/getGeneratorsRootController.ts @@ -1,25 +1,35 @@ import { GeneratorsService } from "../../api/generated/api/resources/generators/service/GeneratorsService"; import { FdrApplication } from "../../app"; -export function getGeneratorsRootController(app: FdrApplication): GeneratorsService { - return new GeneratorsService({ - upsertGenerator: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); +export function getGeneratorsRootController( + app: FdrApplication +): GeneratorsService { + return new GeneratorsService({ + upsertGenerator: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); - await app.dao.generators().upsertGenerator({ generator: req.body }); - return res.send(); - }, - getGenerator: async (req, res) => { - return res.send(await app.dao.generators().getGenerator({ generatorId: req.params.generatorId })); - }, - listGenerators: async (_, res) => { - return res.send(await app.dao.generators().listGenerators()); - }, - getGeneratorByImage: async (req, res) => { - return res.send(await app.dao.generators().getGeneratorByImage({ image: req.body.dockerImage })); - }, - }); + await app.dao.generators().upsertGenerator({ generator: req.body }); + return res.send(); + }, + getGenerator: async (req, res) => { + return res.send( + await app.dao + .generators() + .getGenerator({ generatorId: req.params.generatorId }) + ); + }, + listGenerators: async (_, res) => { + return res.send(await app.dao.generators().listGenerators()); + }, + getGeneratorByImage: async (req, res) => { + return res.send( + await app.dao + .generators() + .getGeneratorByImage({ image: req.body.dockerImage }) + ); + }, + }); } diff --git a/servers/fdr/src/controllers/generators/getGeneratorsVersionsController.ts b/servers/fdr/src/controllers/generators/getGeneratorsVersionsController.ts index 0418217ca6..02435d0736 100644 --- a/servers/fdr/src/controllers/generators/getGeneratorsVersionsController.ts +++ b/servers/fdr/src/controllers/generators/getGeneratorsVersionsController.ts @@ -1,57 +1,68 @@ import { - GeneratorVersionNotFoundError, - NoValidGeneratorsFoundError, + GeneratorVersionNotFoundError, + NoValidGeneratorsFoundError, } from "../../api/generated/api/resources/generators"; import { VersionsService } from "../../api/generated/api/resources/generators/resources/versions/service/VersionsService"; import { FdrApplication } from "../../app"; -export function getGeneratorsVersionsController(app: FdrApplication): VersionsService { - return new VersionsService({ - getLatestGeneratorRelease: async (req, res) => { - const maybeLatestRelease = await app.dao.generatorVersions().getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest: req.body, - }); - if (!maybeLatestRelease) { - throw new NoValidGeneratorsFoundError(); - } +export function getGeneratorsVersionsController( + app: FdrApplication +): VersionsService { + return new VersionsService({ + getLatestGeneratorRelease: async (req, res) => { + const maybeLatestRelease = await app.dao + .generatorVersions() + .getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest: req.body, + }); + if (!maybeLatestRelease) { + throw new NoValidGeneratorsFoundError(); + } - return res.send(maybeLatestRelease); - }, - getChangelog: async (req, res) => { - return res.send( - await app.dao.generatorVersions().getChangelog({ - generator: req.params.generator, - versionRanges: req.body, - }), - ); - }, - upsertGeneratorRelease: async (req, res) => { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: req.headers.authorization, - orgId: "fern", - }); + return res.send(maybeLatestRelease); + }, + getChangelog: async (req, res) => { + return res.send( + await app.dao.generatorVersions().getChangelog({ + generator: req.params.generator, + versionRanges: req.body, + }) + ); + }, + upsertGeneratorRelease: async (req, res) => { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: req.headers.authorization, + orgId: "fern", + }); - await app.dao.generatorVersions().upsertGeneratorRelease({ generatorRelease: req.body }); - return res.send(); - }, - getGeneratorRelease: async (req, res) => { - const maybeRelease = await app.dao - .generatorVersions() - .getGeneratorRelease({ generator: req.params.generator, version: req.params.version }); - if (!maybeRelease) { - throw new GeneratorVersionNotFoundError({ providedVersion: req.params.version }); - } + await app.dao + .generatorVersions() + .upsertGeneratorRelease({ generatorRelease: req.body }); + return res.send(); + }, + getGeneratorRelease: async (req, res) => { + const maybeRelease = await app.dao + .generatorVersions() + .getGeneratorRelease({ + generator: req.params.generator, + version: req.params.version, + }); + if (!maybeRelease) { + throw new GeneratorVersionNotFoundError({ + providedVersion: req.params.version, + }); + } - return res.send(maybeRelease); - }, - listGeneratorReleases: async (req, res) => { - return res.send( - await app.dao.generatorVersions().listGeneratorReleases({ - generator: req.params.generator, - page: req.query.page, - pageSize: req.query.pageSize, - }), - ); - }, - }); + return res.send(maybeRelease); + }, + listGeneratorReleases: async (req, res) => { + return res.send( + await app.dao.generatorVersions().listGeneratorReleases({ + generator: req.params.generator, + page: req.query.page, + pageSize: req.query.pageSize, + }) + ); + }, + }); } diff --git a/servers/fdr/src/controllers/git/getGitController.ts b/servers/fdr/src/controllers/git/getGitController.ts index 0608addddd..4e203d290c 100644 --- a/servers/fdr/src/controllers/git/getGitController.ts +++ b/servers/fdr/src/controllers/git/getGitController.ts @@ -1,103 +1,106 @@ -import { PullRequestNotFoundError, RepositoryNotFoundError } from "../../api/generated/api"; +import { + PullRequestNotFoundError, + RepositoryNotFoundError, +} from "../../api/generated/api"; import { GitService } from "../../api/generated/api/resources/git/service/GitService"; import { FdrApplication } from "../../app"; export function getGitController(app: FdrApplication): GitService { - async function checkIsFernUser(authorization: string | undefined) { - await app.services.auth.checkUserBelongsToOrg({ - authHeader: authorization, - orgId: "fern", - }); - } + async function checkIsFernUser(authorization: string | undefined) { + await app.services.auth.checkUserBelongsToOrg({ + authHeader: authorization, + orgId: "fern", + }); + } - return new GitService({ - getRepository: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + return new GitService({ + getRepository: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - const nameAndOwner = { - repositoryName: req.params.repositoryName, - repositoryOwner: req.params.repositoryOwner, - }; - const maybeRepo = await app.dao.git().getRepository(nameAndOwner); - if (!maybeRepo) { - throw new RepositoryNotFoundError(nameAndOwner); - } + const nameAndOwner = { + repositoryName: req.params.repositoryName, + repositoryOwner: req.params.repositoryOwner, + }; + const maybeRepo = await app.dao.git().getRepository(nameAndOwner); + if (!maybeRepo) { + throw new RepositoryNotFoundError(nameAndOwner); + } - return res.send(maybeRepo); - }, - listRepositories: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + return res.send(maybeRepo); + }, + listRepositories: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - const repos = await app.dao.git().listRepository({ - page: req.body.page, - pageSize: req.body.pageSize, - repositoryName: req.body.repositoryName, - repositoryOwner: req.body.repositoryOwner, - organizationId: req.body.organizationId, - }); + const repos = await app.dao.git().listRepository({ + page: req.body.page, + pageSize: req.body.pageSize, + repositoryName: req.body.repositoryName, + repositoryOwner: req.body.repositoryOwner, + organizationId: req.body.organizationId, + }); - return res.send(repos); - }, - upsertRepository: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + return res.send(repos); + }, + upsertRepository: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - await app.dao.git().upsertRepository({ repository: req.body }); - return res.send(); - }, - deleteRepository: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + await app.dao.git().upsertRepository({ repository: req.body }); + return res.send(); + }, + deleteRepository: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - await app.dao.git().deleteRepository({ - repositoryName: req.params.repositoryName, - repositoryOwner: req.params.repositoryOwner, - }); - return res.send(); - }, - getPullRequest: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + await app.dao.git().deleteRepository({ + repositoryName: req.params.repositoryName, + repositoryOwner: req.params.repositoryOwner, + }); + return res.send(); + }, + getPullRequest: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - const nameAndOwner = { - repositoryName: req.params.repositoryName, - repositoryOwner: req.params.repositoryOwner, - pullRequestNumber: req.params.pullRequestNumber, - }; - const maybePull = await app.dao.git().getPullRequest(nameAndOwner); - if (!maybePull) { - throw new PullRequestNotFoundError(nameAndOwner); - } + const nameAndOwner = { + repositoryName: req.params.repositoryName, + repositoryOwner: req.params.repositoryOwner, + pullRequestNumber: req.params.pullRequestNumber, + }; + const maybePull = await app.dao.git().getPullRequest(nameAndOwner); + if (!maybePull) { + throw new PullRequestNotFoundError(nameAndOwner); + } - return res.send(maybePull); - }, - listPullRequests: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + return res.send(maybePull); + }, + listPullRequests: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - const repos = await app.dao.git().listPullRequests({ - page: req.body.page, - pageSize: req.body.pageSize, - repositoryName: req.body.repositoryName, - repositoryOwner: req.body.repositoryOwner, - organizationId: req.body.organizationId, - state: req.body.state, - author: req.body.author, - }); + const repos = await app.dao.git().listPullRequests({ + page: req.body.page, + pageSize: req.body.pageSize, + repositoryName: req.body.repositoryName, + repositoryOwner: req.body.repositoryOwner, + organizationId: req.body.organizationId, + state: req.body.state, + author: req.body.author, + }); - return res.send(repos); - }, - upsertPullRequest: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + return res.send(repos); + }, + upsertPullRequest: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - await app.dao.git().upsertPullRequest({ pullRequest: req.body }); - return res.send(); - }, - deletePullRequest: async (req, res) => { - await checkIsFernUser(req.headers.authorization); + await app.dao.git().upsertPullRequest({ pullRequest: req.body }); + return res.send(); + }, + deletePullRequest: async (req, res) => { + await checkIsFernUser(req.headers.authorization); - await app.dao.git().deletePullRequest({ - repositoryName: req.params.repositoryName, - repositoryOwner: req.params.repositoryOwner, - pullRequestNumber: req.params.pullRequestNumber, - }); - return res.send(); - }, - }); + await app.dao.git().deletePullRequest({ + repositoryName: req.params.repositoryName, + repositoryOwner: req.params.repositoryOwner, + pullRequestNumber: req.params.pullRequestNumber, + }); + return res.send(); + }, + }); } diff --git a/servers/fdr/src/controllers/sdk/__test__/getLatestVersion.test.ts b/servers/fdr/src/controllers/sdk/__test__/getLatestVersion.test.ts index ed0393ff2a..28dd6f16bc 100644 --- a/servers/fdr/src/controllers/sdk/__test__/getLatestVersion.test.ts +++ b/servers/fdr/src/controllers/sdk/__test__/getLatestVersion.test.ts @@ -1,13 +1,16 @@ -import { getLatestVersionFromNpm, getLatestVersionFromPypi } from "../getLatestVersion"; +import { + getLatestVersionFromNpm, + getLatestVersionFromPypi, +} from "../getLatestVersion"; describe("getLatestVersion", () => { - it("npm", async () => { - const version = await getLatestVersionFromNpm("lodash"); - expect(version).toEqual("4.17.21"); - }); + it("npm", async () => { + const version = await getLatestVersionFromNpm("lodash"); + expect(version).toEqual("4.17.21"); + }); - it("pypi", async () => { - const version = await getLatestVersionFromPypi("qupid"); - expect(version).toEqual("0.1.0"); - }); + it("pypi", async () => { + const version = await getLatestVersionFromPypi("qupid"); + expect(version).toEqual("0.1.0"); + }); }); diff --git a/servers/fdr/src/controllers/sdk/getLatestVersion.ts b/servers/fdr/src/controllers/sdk/getLatestVersion.ts index f14ba8dc78..523ad83826 100644 --- a/servers/fdr/src/controllers/sdk/getLatestVersion.ts +++ b/servers/fdr/src/controllers/sdk/getLatestVersion.ts @@ -1,17 +1,21 @@ import latestVersion from "latest-version"; -export async function getLatestVersionFromNpm(packageName: string): Promise { - return await latestVersion(packageName); +export async function getLatestVersionFromNpm( + packageName: string +): Promise { + return await latestVersion(packageName); } -export async function getLatestVersionFromPypi(packageName: string): Promise { - const response = await fetch(`https://pypi.org/pypi/${packageName}/json`); +export async function getLatestVersionFromPypi( + packageName: string +): Promise { + const response = await fetch(`https://pypi.org/pypi/${packageName}/json`); - if (response.ok) { - // Extract the latest version from the response data - const packageData = (await response.json()) as any; - return packageData.info.version; - } + if (response.ok) { + // Extract the latest version from the response data + const packageData = (await response.json()) as any; + return packageData.info.version; + } - return undefined; + return undefined; } diff --git a/servers/fdr/src/controllers/sdk/getVersionsService.ts b/servers/fdr/src/controllers/sdk/getVersionsService.ts index f061ceab74..8d5dab63b6 100644 --- a/servers/fdr/src/controllers/sdk/getVersionsService.ts +++ b/servers/fdr/src/controllers/sdk/getVersionsService.ts @@ -1,77 +1,80 @@ import { getLatestTag } from "@fern-api/github"; import semver from "semver"; import { - FailedToComputeExistingVersion, - FailedToIncrementVersion, - Language, + FailedToComputeExistingVersion, + FailedToIncrementVersion, + Language, } from "../../api/generated/api/resources/sdks"; import { VersionsService } from "../../api/generated/api/resources/sdks/resources/versions/service/VersionsService"; import { FdrApplication } from "../../app"; -import { getLatestVersionFromNpm, getLatestVersionFromPypi } from "./getLatestVersion"; +import { + getLatestVersionFromNpm, + getLatestVersionFromPypi, +} from "./getLatestVersion"; export function getVersionsService(app: FdrApplication): VersionsService { - return new VersionsService({ - computeSemanticVersion: async (req, res) => { - const existingVersion = await getExistingVersion({ - githubRepository: req.body.githubRepository, - packageName: req.body.package, - language: req.body.language, - }); - if (existingVersion == null) { - throw new FailedToComputeExistingVersion(); - } + return new VersionsService({ + computeSemanticVersion: async (req, res) => { + const existingVersion = await getExistingVersion({ + githubRepository: req.body.githubRepository, + packageName: req.body.package, + language: req.body.language, + }); + if (existingVersion == null) { + throw new FailedToComputeExistingVersion(); + } - // TODO(armando): make this more robust by factoring in api definition changes - const nextVersion = semver.inc(existingVersion, "patch"); - if (nextVersion == null) { - throw new FailedToIncrementVersion(); - } + // TODO(armando): make this more robust by factoring in api definition changes + const nextVersion = semver.inc(existingVersion, "patch"); + if (nextVersion == null) { + throw new FailedToIncrementVersion(); + } - return res.send({ - version: nextVersion, - bump: "PATCH", - }); - }, - }); + return res.send({ + version: nextVersion, + bump: "PATCH", + }); + }, + }); } // NOTE: this does not handle private registries or private github repositories export async function getExistingVersion({ - packageName, - language, - githubRepository, + packageName, + language, + githubRepository, }: { - packageName: string; - language: Language; - githubRepository: string | undefined; + packageName: string; + language: Language; + githubRepository: string | undefined; }): Promise { - let version: string | undefined = undefined; + let version: string | undefined = undefined; - // Step 1: Fetch from registries directly - switch (language) { - case "TypeScript": - version = await getLatestVersionFromNpm(packageName); - break; - case "Python": - version = await getLatestVersionFromPypi(packageName); - break; - case "Csharp": - break; - case "Go": - break; - case "Java": - break; - case "Ruby": - break; - } - if (version != null) { - return version; - } + // Step 1: Fetch from registries directly + switch (language) { + case "TypeScript": + version = await getLatestVersionFromNpm(packageName); + break; + case "Python": + version = await getLatestVersionFromPypi(packageName); + break; + case "Csharp": + break; + case "Go": + break; + case "Java": + break; + case "Ruby": + break; + } + if (version != null) { + return version; + } - // Step 2: Fetch from Github Tag - if (githubRepository != null) { - version = await getLatestTag(githubRepository); - } + // Step 2: Fetch from Github Tag + if (githubRepository != null) { + version = await getLatestTag(githubRepository); + } - return version; + return version; } diff --git a/servers/fdr/src/controllers/snippets/APIResolver.ts b/servers/fdr/src/controllers/snippets/APIResolver.ts index 87929893b3..19944ca437 100644 --- a/servers/fdr/src/controllers/snippets/APIResolver.ts +++ b/servers/fdr/src/controllers/snippets/APIResolver.ts @@ -1,87 +1,107 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; -import { ApiIdRequiredError, OrgIdAndApiIdNotFound } from "../../api/generated/api/resources/snippets/errors"; +import { + ApiIdRequiredError, + OrgIdAndApiIdNotFound, +} from "../../api/generated/api/resources/snippets/errors"; import { FdrApplication } from "../../app"; import { AuthUtility } from "./AuthUtils"; export interface ResolvedAPI { - apiId: FdrAPI.ApiId; - orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + orgId: FdrAPI.OrgId; } export class APIResolver { - private authUtil: AuthUtility; - constructor( - private readonly app: FdrApplication, - authHeader: string, - ) { - this.authUtil = new AuthUtility(app, authHeader); - } + private authUtil: AuthUtility; + constructor( + private readonly app: FdrApplication, + authHeader: string + ) { + this.authUtil = new AuthUtility(app, authHeader); + } - public async resolve(): Promise { - const orgId = await this.authUtil.inferOrg(); - return this.resolveWithOrgId({ orgId }); - } + public async resolve(): Promise { + const orgId = await this.authUtil.inferOrg(); + return this.resolveWithOrgId({ orgId }); + } - public async resolveWithApiId({ apiId }: { apiId: FdrAPI.ApiId }): Promise { - const orgId = await this.authUtil.inferOrg(); - return this.resolveWithOrgAndApiId({ orgId, apiId }); - } + public async resolveWithApiId({ + apiId, + }: { + apiId: FdrAPI.ApiId; + }): Promise { + const orgId = await this.authUtil.inferOrg(); + return this.resolveWithOrgAndApiId({ orgId, apiId }); + } - public async resolveWithOrgId({ orgId }: { orgId: FdrAPI.OrgId }): Promise { - const apiInfos = await this.app.dao.snippetAPIs().loadSnippetAPIs({ - loadSnippetAPIsRequest: { - orgIds: [orgId], - apiName: undefined, - }, - }); - if (apiInfos.length > 1) { - throw new ApiIdRequiredError("Multiple APIs were found; please provide an apiId"); - } - const inferredApi = Array.from(apiInfos)[0]; - if (inferredApi == null) { - throw new ApiIdRequiredError("No APIs were found; have you triggered SDK generation and publishing?"); - } - return this.resolveWithOrgAndApiId({ orgId, apiId: FdrAPI.ApiId(inferredApi.apiName) }); + public async resolveWithOrgId({ + orgId, + }: { + orgId: FdrAPI.OrgId; + }): Promise { + const apiInfos = await this.app.dao.snippetAPIs().loadSnippetAPIs({ + loadSnippetAPIsRequest: { + orgIds: [orgId], + apiName: undefined, + }, + }); + if (apiInfos.length > 1) { + throw new ApiIdRequiredError( + "Multiple APIs were found; please provide an apiId" + ); + } + const inferredApi = Array.from(apiInfos)[0]; + if (inferredApi == null) { + throw new ApiIdRequiredError( + "No APIs were found; have you triggered SDK generation and publishing?" + ); } + return this.resolveWithOrgAndApiId({ + orgId, + apiId: FdrAPI.ApiId(inferredApi.apiName), + }); + } - public async resolveWithOrgAndApiId({ + public async resolveWithOrgAndApiId({ + orgId, + apiId, + }: { + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + }): Promise { + await this.authUtil.assertUserHasAccessToOrg(orgId); + const snippetAPI = await this.app.dao.snippetAPIs().loadSnippetAPI({ + loadSnippetAPIRequest: { orgId, - apiId, - }: { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - }): Promise { - await this.authUtil.assertUserHasAccessToOrg(orgId); - const snippetAPI = await this.app.dao.snippetAPIs().loadSnippetAPI({ - loadSnippetAPIRequest: { - orgId, - apiName: apiId, - }, - }); - if (snippetAPI === null) { - throw new OrgIdAndApiIdNotFound(`Organization ${orgId} does not have API ${apiId}`); - } - return { - orgId: FdrAPI.OrgId(snippetAPI.orgId), - apiId: FdrAPI.ApiId(snippetAPI.apiName), - }; + apiName: apiId, + }, + }); + if (snippetAPI === null) { + throw new OrgIdAndApiIdNotFound( + `Organization ${orgId} does not have API ${apiId}` + ); } + return { + orgId: FdrAPI.OrgId(snippetAPI.orgId), + apiId: FdrAPI.ApiId(snippetAPI.apiName), + }; + } - public async resolveApi({ - orgId, - apiId, - }: { - orgId: FdrAPI.OrgId | undefined; - apiId: FdrAPI.ApiId | undefined; - }): Promise { - if (orgId != null && apiId != null) { - return await this.resolveWithOrgAndApiId({ orgId, apiId }); - } else if (orgId != null && apiId == null) { - return await this.resolveWithOrgId({ orgId }); - } else if (orgId == null && apiId != null) { - return await this.resolveWithApiId({ apiId }); - } else { - return await this.resolve(); - } + public async resolveApi({ + orgId, + apiId, + }: { + orgId: FdrAPI.OrgId | undefined; + apiId: FdrAPI.ApiId | undefined; + }): Promise { + if (orgId != null && apiId != null) { + return await this.resolveWithOrgAndApiId({ orgId, apiId }); + } else if (orgId != null && apiId == null) { + return await this.resolveWithOrgId({ orgId }); + } else if (orgId == null && apiId != null) { + return await this.resolveWithApiId({ apiId }); + } else { + return await this.resolve(); } + } } diff --git a/servers/fdr/src/controllers/snippets/AuthUtils.ts b/servers/fdr/src/controllers/snippets/AuthUtils.ts index 50649c31e0..b4bd9d9733 100644 --- a/servers/fdr/src/controllers/snippets/AuthUtils.ts +++ b/servers/fdr/src/controllers/snippets/AuthUtils.ts @@ -1,39 +1,48 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { UnauthorizedError } from "../../api/generated/api/resources/commons/errors"; -import { OrgIdNotFound, OrgIdRequiredError } from "../../api/generated/api/resources/snippets/errors"; +import { + OrgIdNotFound, + OrgIdRequiredError, +} from "../../api/generated/api/resources/snippets/errors"; import { FdrApplication } from "../../app"; export class AuthUtility { - constructor( - private readonly app: FdrApplication, - private readonly authHeader: string, - ) {} - public async inferOrg(): Promise { - const orgIds = await this.getOrgIds(); - if (orgIds.size > 1) { - throw new OrgIdRequiredError("Your user has access to multiple organizations. Please provide an orgId"); - } - const inferredOrgId = Array.from(orgIds)[0]; - if (inferredOrgId == null) { - throw new OrgIdNotFound("No organizations were resolved for this user"); - } - return FdrAPI.OrgId(inferredOrgId); + constructor( + private readonly app: FdrApplication, + private readonly authHeader: string + ) {} + public async inferOrg(): Promise { + const orgIds = await this.getOrgIds(); + if (orgIds.size > 1) { + throw new OrgIdRequiredError( + "Your user has access to multiple organizations. Please provide an orgId" + ); } + const inferredOrgId = Array.from(orgIds)[0]; + if (inferredOrgId == null) { + throw new OrgIdNotFound("No organizations were resolved for this user"); + } + return FdrAPI.OrgId(inferredOrgId); + } - public async assertUserHasAccessToOrg(orgId: string) { - const orgIds = await this.getOrgIds(); - if (!orgIds.has(orgId)) { - throw new UnauthorizedError(`You are not a member of organization ${orgId}`); - } + public async assertUserHasAccessToOrg(orgId: string) { + const orgIds = await this.getOrgIds(); + if (!orgIds.has(orgId)) { + throw new UnauthorizedError( + `You are not a member of organization ${orgId}` + ); } + } - public async getOrgIds(): Promise> { - const orgIdsResponse = await this.app.services.auth.getOrgIdsFromAuthHeader({ - authHeader: this.authHeader, - }); - if (orgIdsResponse.type === "error") { - throw orgIdsResponse.err; - } - return orgIdsResponse.orgIds; + public async getOrgIds(): Promise> { + const orgIdsResponse = await this.app.services.auth.getOrgIdsFromAuthHeader( + { + authHeader: this.authHeader, + } + ); + if (orgIdsResponse.type === "error") { + throw orgIdsResponse.err; } + return orgIdsResponse.orgIds; + } } diff --git a/servers/fdr/src/controllers/snippets/getSnippetsFactoryService.ts b/servers/fdr/src/controllers/snippets/getSnippetsFactoryService.ts index e7a7af5fee..58bfc01b6a 100644 --- a/servers/fdr/src/controllers/snippets/getSnippetsFactoryService.ts +++ b/servers/fdr/src/controllers/snippets/getSnippetsFactoryService.ts @@ -1,17 +1,19 @@ import { SnippetsFactoryService } from "../../api"; import { type FdrApplication } from "../../app"; -export function getSnippetsFactoryService(app: FdrApplication): SnippetsFactoryService { - return new SnippetsFactoryService({ - createSnippetsForSdk: async (req, res) => { - await app.dao.snippets().storeSnippets({ - storeSnippetsInfo: { - orgId: req.body.orgId, - apiId: req.body.apiId, - sdk: req.body.snippets, - }, - }); - return res.send(); +export function getSnippetsFactoryService( + app: FdrApplication +): SnippetsFactoryService { + return new SnippetsFactoryService({ + createSnippetsForSdk: async (req, res) => { + await app.dao.snippets().storeSnippets({ + storeSnippetsInfo: { + orgId: req.body.orgId, + apiId: req.body.apiId, + sdk: req.body.snippets, }, - }); + }); + return res.send(); + }, + }); } diff --git a/servers/fdr/src/controllers/snippets/getSnippetsService.ts b/servers/fdr/src/controllers/snippets/getSnippetsService.ts index 494fdacd9a..6f686a021f 100644 --- a/servers/fdr/src/controllers/snippets/getSnippetsService.ts +++ b/servers/fdr/src/controllers/snippets/getSnippetsService.ts @@ -1,127 +1,138 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { SnippetTemplateResolver } from "@fern-api/template-resolver"; import { SnippetsService } from "../../api"; -import { InvalidPageError, SnippetTemplateNotFoundError, UnauthorizedError } from "../../api/generated/api"; +import { + InvalidPageError, + SnippetTemplateNotFoundError, + UnauthorizedError, +} from "../../api/generated/api"; import { type FdrApplication } from "../../app"; import { DbSnippetsPage } from "../../db/snippets/SnippetsDao"; import { APIResolver } from "./APIResolver"; export function getSnippetsService(app: FdrApplication): SnippetsService { - return new SnippetsService({ - get: async (req, res) => { - if (req.headers.authorization === undefined) { - throw new UnauthorizedError("You must be authorized to load snippets"); - } - const apiInferrer = new APIResolver(app, req.headers.authorization); - const apiInfo = await apiInferrer.resolveApi({ - orgId: req.body.orgId, - apiId: req.body.apiId, - }); - await app.services.auth.checkOrgHasSnippetsApiAccess({ - authHeader: req.headers.authorization, - orgId: apiInfo.orgId, - failHard: true, - }); - const payload = req.body.payload; - if (payload == null) { - const response: DbSnippetsPage = await app.dao.snippets().loadSnippetsPage({ - loadSnippetsInfo: { - orgId: apiInfo.orgId, - apiId: apiInfo.apiId, - endpointIdentifier: req.body.endpoint, - exampleIdentifier: req.body.exampleIdentifier, - sdks: req.body.sdks, - page: undefined, - }, - }); - - let snippetsForEndpoint; - if (req.body.endpoint.identifierOverride != null) { - snippetsForEndpoint = response.snippetsByEndpointId[req.body.endpoint.identifierOverride]; - } + return new SnippetsService({ + get: async (req, res) => { + if (req.headers.authorization === undefined) { + throw new UnauthorizedError("You must be authorized to load snippets"); + } + const apiInferrer = new APIResolver(app, req.headers.authorization); + const apiInfo = await apiInferrer.resolveApi({ + orgId: req.body.orgId, + apiId: req.body.apiId, + }); + await app.services.auth.checkOrgHasSnippetsApiAccess({ + authHeader: req.headers.authorization, + orgId: apiInfo.orgId, + failHard: true, + }); + const payload = req.body.payload; + if (payload == null) { + const response: DbSnippetsPage = await app.dao + .snippets() + .loadSnippetsPage({ + loadSnippetsInfo: { + orgId: apiInfo.orgId, + apiId: apiInfo.apiId, + endpointIdentifier: req.body.endpoint, + exampleIdentifier: req.body.exampleIdentifier, + sdks: req.body.sdks, + page: undefined, + }, + }); - // If you have any snippets from using the identifierOverride, you're set, but if not or if the override isn't - // specified, you'll need to go leverage the legacy route (path + method). - if ( - req.body.endpoint.identifierOverride == null || - snippetsForEndpoint == undefined || - snippetsForEndpoint.length == 0 - ) { - const snippetsForEndpointPath = response.snippets[req.body.endpoint.path]; - if (snippetsForEndpointPath === undefined) { - return res.send([]); - } - const snippetsForEndpointMethod = snippetsForEndpointPath[req.body.endpoint.method]; - snippetsForEndpoint = snippetsForEndpointMethod ?? []; - } - return res.send(snippetsForEndpoint ?? []); - } else { - try { - const snippets: FdrAPI.Snippet[] = []; + let snippetsForEndpoint; + if (req.body.endpoint.identifierOverride != null) { + snippetsForEndpoint = + response.snippetsByEndpointId[req.body.endpoint.identifierOverride]; + } - for (const sdk of req.body.sdks ?? []) { - const endpointSnippetTemplate: FdrAPI.EndpointSnippetTemplate | null = await app.dao - .snippetTemplates() - .loadSnippetTemplate({ - loadSnippetTemplateRequest: { - orgId: apiInfo.orgId, - apiId: apiInfo.apiId, - endpointId: req.body.endpoint, - sdk, - }, - }); - if (endpointSnippetTemplate == null) { - throw new SnippetTemplateNotFoundError("Snippet not found"); - } - const templateResolver = new SnippetTemplateResolver({ - payload, - endpointSnippetTemplate, - }); + // If you have any snippets from using the identifierOverride, you're set, but if not or if the override isn't + // specified, you'll need to go leverage the legacy route (path + method). + if ( + req.body.endpoint.identifierOverride == null || + snippetsForEndpoint == undefined || + snippetsForEndpoint.length == 0 + ) { + const snippetsForEndpointPath = + response.snippets[req.body.endpoint.path]; + if (snippetsForEndpointPath === undefined) { + return res.send([]); + } + const snippetsForEndpointMethod = + snippetsForEndpointPath[req.body.endpoint.method]; + snippetsForEndpoint = snippetsForEndpointMethod ?? []; + } + return res.send(snippetsForEndpoint ?? []); + } else { + try { + const snippets: FdrAPI.Snippet[] = []; - snippets.push(templateResolver.resolve()); - } - - return await res.send(snippets); - } catch (e) { - return await res.send([]); - } - } - }, - load: async (req, res) => { - if (req.headers.authorization === undefined) { - throw new UnauthorizedError("You must be authorized to load snippets"); - } - const apiInferrer = new APIResolver(app, req.headers.authorization); - const apiInfo = await apiInferrer.resolveApi({ - orgId: req.body.orgId, - apiId: req.body.apiId, - }); - await app.services.auth.checkOrgHasSnippetsApiAccess({ - authHeader: req.headers.authorization, - orgId: apiInfo.orgId, - failHard: true, - }); - // TODO: The cast shouldn't be necessary but the query parameter is being - // passed in as a string (even though it's typed as a number), so we - // need to use the + operator to make it a number. - const page: number | undefined = req.query.page !== undefined ? +req.query.page : undefined; - if (page !== undefined && page <= 0) { - throw new InvalidPageError("Query parameter 'page' must be >= 1"); - } - const response: DbSnippetsPage = await app.dao.snippets().loadSnippetsPage({ - loadSnippetsInfo: { - orgId: apiInfo.orgId, - apiId: apiInfo.apiId, - endpointIdentifier: undefined, - exampleIdentifier: undefined, - sdks: req.body.sdks, - page, + for (const sdk of req.body.sdks ?? []) { + const endpointSnippetTemplate: FdrAPI.EndpointSnippetTemplate | null = + await app.dao.snippetTemplates().loadSnippetTemplate({ + loadSnippetTemplateRequest: { + orgId: apiInfo.orgId, + apiId: apiInfo.apiId, + endpointId: req.body.endpoint, + sdk, }, + }); + if (endpointSnippetTemplate == null) { + throw new SnippetTemplateNotFoundError("Snippet not found"); + } + const templateResolver = new SnippetTemplateResolver({ + payload, + endpointSnippetTemplate, }); - return res.send({ - next: response.nextPage, - snippets: response.snippets, - }); - }, - }); + + snippets.push(templateResolver.resolve()); + } + + return await res.send(snippets); + } catch (e) { + return await res.send([]); + } + } + }, + load: async (req, res) => { + if (req.headers.authorization === undefined) { + throw new UnauthorizedError("You must be authorized to load snippets"); + } + const apiInferrer = new APIResolver(app, req.headers.authorization); + const apiInfo = await apiInferrer.resolveApi({ + orgId: req.body.orgId, + apiId: req.body.apiId, + }); + await app.services.auth.checkOrgHasSnippetsApiAccess({ + authHeader: req.headers.authorization, + orgId: apiInfo.orgId, + failHard: true, + }); + // TODO: The cast shouldn't be necessary but the query parameter is being + // passed in as a string (even though it's typed as a number), so we + // need to use the + operator to make it a number. + const page: number | undefined = + req.query.page !== undefined ? +req.query.page : undefined; + if (page !== undefined && page <= 0) { + throw new InvalidPageError("Query parameter 'page' must be >= 1"); + } + const response: DbSnippetsPage = await app.dao + .snippets() + .loadSnippetsPage({ + loadSnippetsInfo: { + orgId: apiInfo.orgId, + apiId: apiInfo.apiId, + endpointIdentifier: undefined, + exampleIdentifier: undefined, + sdks: req.body.sdks, + page, + }, + }); + return res.send({ + next: response.nextPage, + snippets: response.snippets, + }); + }, + }); } diff --git a/servers/fdr/src/controllers/snippets/getTemplatesService.ts b/servers/fdr/src/controllers/snippets/getTemplatesService.ts index 8f629f4987..56f495731e 100644 --- a/servers/fdr/src/controllers/snippets/getTemplatesService.ts +++ b/servers/fdr/src/controllers/snippets/getTemplatesService.ts @@ -4,101 +4,101 @@ import { type FdrApplication } from "../../app"; import { APIResolver } from "./APIResolver"; export function getTemplatesService(app: FdrApplication): TemplatesService { - return new TemplatesService({ - register: async (req, res) => { - // if (req.headers.authorization === undefined) { - // throw new UnauthorizedError("You must be authorized to load snippets"); - // } - // const apiInferrer = new APIResolver(app, req.headers.authorization); - // const apiInfo = await apiInferrer.resolveApi({ - // orgId: req.body.orgId, - // apiId: req.body.apiId, - // }); - // await app.services.auth.checkOrgHasSnippetTemplateAccess({ - // authHeader: req.headers.authorization, - // orgId: apiInfo.orgId, - // failHard: true, - // }); - const api = await app.dao.snippetAPIs().loadSnippetAPI({ - loadSnippetAPIRequest: { - orgId: req.body.orgId, - apiName: req.body.apiId, - }, - }); - if (api == null) { - await app.dao.snippetAPIs().createSnippetAPI({ - apiName: req.body.apiId, - orgId: req.body.orgId, - }); - } - await app.dao.snippetTemplates().storeSnippetTemplate({ - storeSnippetsInfo: { - ...req.body, - snippets: [req.body.snippet], - // Override the specified org and API to match what they're authed to do - // orgId: apiInfo.orgId, - // apiId: apiInfo.apiId, - }, - }); - return res.send(); + return new TemplatesService({ + register: async (req, res) => { + // if (req.headers.authorization === undefined) { + // throw new UnauthorizedError("You must be authorized to load snippets"); + // } + // const apiInferrer = new APIResolver(app, req.headers.authorization); + // const apiInfo = await apiInferrer.resolveApi({ + // orgId: req.body.orgId, + // apiId: req.body.apiId, + // }); + // await app.services.auth.checkOrgHasSnippetTemplateAccess({ + // authHeader: req.headers.authorization, + // orgId: apiInfo.orgId, + // failHard: true, + // }); + const api = await app.dao.snippetAPIs().loadSnippetAPI({ + loadSnippetAPIRequest: { + orgId: req.body.orgId, + apiName: req.body.apiId, }, - registerBatch: async (req, res) => { - // if (req.headers.authorization === undefined) { - // throw new UnauthorizedError("You must be authorized to load snippets"); - // } - // const apiInferrer = new APIResolver(app, req.headers.authorization); - // const apiInfo = await apiInferrer.resolveApi({ - // orgId: req.body.orgId, - // apiId: req.body.apiId, - // }); - // await app.services.auth.checkOrgHasSnippetTemplateAccess({ - // authHeader: req.headers.authorization, - // orgId: apiInfo.orgId, - // failHard: true, - // }); - const api = await app.dao.snippetAPIs().loadSnippetAPI({ - loadSnippetAPIRequest: { - orgId: req.body.orgId, - apiName: req.body.apiId, - }, - }); - if (api == null) { - await app.dao.snippetAPIs().createSnippetAPI({ - apiName: req.body.apiId, - orgId: req.body.orgId, - }); - } - await app.dao.snippetTemplates().storeSnippetTemplate({ - storeSnippetsInfo: { - ...req.body, - // Override the specified org and API to match what they're authed to do - // orgId: apiInfo.orgId, - // apiId: apiInfo.apiId, - }, - }); - return res.send(); + }); + if (api == null) { + await app.dao.snippetAPIs().createSnippetAPI({ + apiName: req.body.apiId, + orgId: req.body.orgId, + }); + } + await app.dao.snippetTemplates().storeSnippetTemplate({ + storeSnippetsInfo: { + ...req.body, + snippets: [req.body.snippet], + // Override the specified org and API to match what they're authed to do + // orgId: apiInfo.orgId, + // apiId: apiInfo.apiId, }, - get: async (req, res) => { - if (req.headers.authorization === undefined) { - throw new UnauthorizedError("You must be authorized to load snippets"); - } - const apiInferrer = new APIResolver(app, req.headers.authorization); - const apiInfo = await apiInferrer.resolveApi({ - orgId: req.body.orgId, - apiId: req.body.apiId, - }); - const snippet = await app.dao.snippetTemplates().loadSnippetTemplate({ - loadSnippetTemplateRequest: { - ...req.body, - // Override the specified org and API to match what they're authed to do - orgId: apiInfo.orgId, - apiId: apiInfo.apiId, - }, - }); - if (snippet == null) { - throw new SnippetNotFound("The requested snippet could not be found."); - } - return res.send(snippet); + }); + return res.send(); + }, + registerBatch: async (req, res) => { + // if (req.headers.authorization === undefined) { + // throw new UnauthorizedError("You must be authorized to load snippets"); + // } + // const apiInferrer = new APIResolver(app, req.headers.authorization); + // const apiInfo = await apiInferrer.resolveApi({ + // orgId: req.body.orgId, + // apiId: req.body.apiId, + // }); + // await app.services.auth.checkOrgHasSnippetTemplateAccess({ + // authHeader: req.headers.authorization, + // orgId: apiInfo.orgId, + // failHard: true, + // }); + const api = await app.dao.snippetAPIs().loadSnippetAPI({ + loadSnippetAPIRequest: { + orgId: req.body.orgId, + apiName: req.body.apiId, }, - }); + }); + if (api == null) { + await app.dao.snippetAPIs().createSnippetAPI({ + apiName: req.body.apiId, + orgId: req.body.orgId, + }); + } + await app.dao.snippetTemplates().storeSnippetTemplate({ + storeSnippetsInfo: { + ...req.body, + // Override the specified org and API to match what they're authed to do + // orgId: apiInfo.orgId, + // apiId: apiInfo.apiId, + }, + }); + return res.send(); + }, + get: async (req, res) => { + if (req.headers.authorization === undefined) { + throw new UnauthorizedError("You must be authorized to load snippets"); + } + const apiInferrer = new APIResolver(app, req.headers.authorization); + const apiInfo = await apiInferrer.resolveApi({ + orgId: req.body.orgId, + apiId: req.body.apiId, + }); + const snippet = await app.dao.snippetTemplates().loadSnippetTemplate({ + loadSnippetTemplateRequest: { + ...req.body, + // Override the specified org and API to match what they're authed to do + orgId: apiInfo.orgId, + apiId: apiInfo.apiId, + }, + }); + if (snippet == null) { + throw new SnippetNotFound("The requested snippet could not be found."); + } + return res.send(snippet); + }, + }); } diff --git a/servers/fdr/src/controllers/tokens/getTokensService.ts b/servers/fdr/src/controllers/tokens/getTokensService.ts index db26023f49..04d4984e65 100644 --- a/servers/fdr/src/controllers/tokens/getTokensService.ts +++ b/servers/fdr/src/controllers/tokens/getTokensService.ts @@ -6,30 +6,32 @@ import { type FdrApplication } from "../../app"; import { getTokenFromAuthHeader } from "../../services/auth/AuthService"; export function getTokensService(app: FdrApplication): TokensService { - return new TokensService({ - generate: async (req, res) => { - const authorization = req.headers.authorization; - if (authorization == null) { - throw new UnauthorizedError("No token specified. Please use your FERN_TOKEN"); - } - const token = getTokenFromAuthHeader(authorization); - const venus = new FernVenusApiClient({ - environment: app.config.venusUrl, - token, - }); - const response = await venus.registry.generateRegistryTokens({ - organizationId: FernVenusApi.OrganizationId(req.body.orgId), - }); - if (response.ok) { - return res.send({ - id: uuidv4(), - token: response.body.npm.token, - }); - } - throw new Error("Failed to generate token."); - }, - revoke: (req, res) => { - return res.send(); - }, - }); + return new TokensService({ + generate: async (req, res) => { + const authorization = req.headers.authorization; + if (authorization == null) { + throw new UnauthorizedError( + "No token specified. Please use your FERN_TOKEN" + ); + } + const token = getTokenFromAuthHeader(authorization); + const venus = new FernVenusApiClient({ + environment: app.config.venusUrl, + token, + }); + const response = await venus.registry.generateRegistryTokens({ + organizationId: FernVenusApi.OrganizationId(req.body.orgId), + }); + if (response.ok) { + return res.send({ + id: uuidv4(), + token: response.body.npm.token, + }); + } + throw new Error("Failed to generate token."); + }, + revoke: (req, res) => { + return res.send(); + }, + }); } diff --git a/servers/fdr/src/db/FdrDao.ts b/servers/fdr/src/db/FdrDao.ts index 86e4e513ad..4d52122acb 100644 --- a/servers/fdr/src/db/FdrDao.ts +++ b/servers/fdr/src/db/FdrDao.ts @@ -1,91 +1,100 @@ import { PrismaClient } from "@prisma/client"; import { APIDefinitionDao, APIDefinitionDaoImpl } from "./api/APIDefinitionDao"; import { DocsV2Dao, DocsV2DaoImpl } from "./docs/DocsV2Dao"; -import { IndexSegmentDaoImpl, type IndexSegmentDao } from "./docs/IndexSegmentDao"; +import { + IndexSegmentDaoImpl, + type IndexSegmentDao, +} from "./docs/IndexSegmentDao"; import { CliVersionsDaoImpl } from "./generators/CliVersionsDao"; import { GeneratorsDaoImpl } from "./generators/GeneratorDao"; import { GeneratorVersionsDaoImpl } from "./generators/GeneratorVersionsDao"; import { GitDaoImpl } from "./git/GitDao"; import { DocsRegistrationDao } from "./registrations/DocsRegistrationDao"; import { SdkDao, SdkDaoImpl } from "./sdk/SdkDao"; -import { SnippetAPIsDaoImpl, type SnippetAPIsDao } from "./snippetApis/SnippetAPIsDao"; -import { SnippetTemplateDao, SnippetTemplateDaoImpl } from "./snippets/SnippetTemplate"; +import { + SnippetAPIsDaoImpl, + type SnippetAPIsDao, +} from "./snippetApis/SnippetAPIsDao"; +import { + SnippetTemplateDao, + SnippetTemplateDaoImpl, +} from "./snippets/SnippetTemplate"; import { SnippetsDaoImpl, type SnippetsDao } from "./snippets/SnippetsDao"; export class FdrDao { - private docsV2Dao; - private apisDao; - private indexSegmentDao; - private snippetsDao; - private snippetTemplateDao; - private snippetAPIsDao; - private sdksDao; - private docsRegistrationDao; - private generatorsDao; - private generatorVersionsDao; - private cliVersionsDao; - private gitDao; + private docsV2Dao; + private apisDao; + private indexSegmentDao; + private snippetsDao; + private snippetTemplateDao; + private snippetAPIsDao; + private sdksDao; + private docsRegistrationDao; + private generatorsDao; + private generatorVersionsDao; + private cliVersionsDao; + private gitDao; - constructor(prisma: PrismaClient) { - this.docsV2Dao = new DocsV2DaoImpl(prisma); - this.apisDao = new APIDefinitionDaoImpl(prisma); - this.indexSegmentDao = new IndexSegmentDaoImpl(prisma); - this.snippetsDao = new SnippetsDaoImpl(prisma); - this.snippetAPIsDao = new SnippetAPIsDaoImpl(prisma); - this.sdksDao = new SdkDaoImpl(prisma); - this.snippetTemplateDao = new SnippetTemplateDaoImpl(prisma); - this.docsRegistrationDao = new DocsRegistrationDao(prisma); - this.generatorsDao = new GeneratorsDaoImpl(prisma); - this.generatorVersionsDao = new GeneratorVersionsDaoImpl(prisma); - this.cliVersionsDao = new CliVersionsDaoImpl(prisma); - this.gitDao = new GitDaoImpl(prisma); - } + constructor(prisma: PrismaClient) { + this.docsV2Dao = new DocsV2DaoImpl(prisma); + this.apisDao = new APIDefinitionDaoImpl(prisma); + this.indexSegmentDao = new IndexSegmentDaoImpl(prisma); + this.snippetsDao = new SnippetsDaoImpl(prisma); + this.snippetAPIsDao = new SnippetAPIsDaoImpl(prisma); + this.sdksDao = new SdkDaoImpl(prisma); + this.snippetTemplateDao = new SnippetTemplateDaoImpl(prisma); + this.docsRegistrationDao = new DocsRegistrationDao(prisma); + this.generatorsDao = new GeneratorsDaoImpl(prisma); + this.generatorVersionsDao = new GeneratorVersionsDaoImpl(prisma); + this.cliVersionsDao = new CliVersionsDaoImpl(prisma); + this.gitDao = new GitDaoImpl(prisma); + } - public docsV2(): DocsV2Dao { - return this.docsV2Dao; - } + public docsV2(): DocsV2Dao { + return this.docsV2Dao; + } - public apis(): APIDefinitionDao { - return this.apisDao; - } + public apis(): APIDefinitionDao { + return this.apisDao; + } - public indexSegment(): IndexSegmentDao { - return this.indexSegmentDao; - } + public indexSegment(): IndexSegmentDao { + return this.indexSegmentDao; + } - public snippets(): SnippetsDao { - return this.snippetsDao; - } + public snippets(): SnippetsDao { + return this.snippetsDao; + } - public snippetAPIs(): SnippetAPIsDao { - return this.snippetAPIsDao; - } + public snippetAPIs(): SnippetAPIsDao { + return this.snippetAPIsDao; + } - public snippetTemplates(): SnippetTemplateDao { - return this.snippetTemplateDao; - } + public snippetTemplates(): SnippetTemplateDao { + return this.snippetTemplateDao; + } - public sdks(): SdkDao { - return this.sdksDao; - } + public sdks(): SdkDao { + return this.sdksDao; + } - public docsRegistration(): DocsRegistrationDao { - return this.docsRegistrationDao; - } + public docsRegistration(): DocsRegistrationDao { + return this.docsRegistrationDao; + } - public generators(): GeneratorsDaoImpl { - return this.generatorsDao; - } + public generators(): GeneratorsDaoImpl { + return this.generatorsDao; + } - public generatorVersions(): GeneratorVersionsDaoImpl { - return this.generatorVersionsDao; - } + public generatorVersions(): GeneratorVersionsDaoImpl { + return this.generatorVersionsDao; + } - public cliVersions(): CliVersionsDaoImpl { - return this.cliVersionsDao; - } + public cliVersions(): CliVersionsDaoImpl { + return this.cliVersionsDao; + } - public git(): GitDaoImpl { - return this.gitDao; - } + public git(): GitDaoImpl { + return this.gitDao; + } } diff --git a/servers/fdr/src/db/api/APIDefinitionDao.ts b/servers/fdr/src/db/api/APIDefinitionDao.ts index 4d33b4f91b..6eed4d3fa4 100644 --- a/servers/fdr/src/db/api/APIDefinitionDao.ts +++ b/servers/fdr/src/db/api/APIDefinitionDao.ts @@ -3,59 +3,74 @@ import { PrismaClient } from "@prisma/client"; import { readBuffer } from "../../util"; export interface APIDefinitionDao { - getOrgIdForApiDefinition(apiDefinitionId: string): Promise; + getOrgIdForApiDefinition( + apiDefinitionId: string + ): Promise; - loadAPIDefinition(apiDefinitionId: string): Promise; + loadAPIDefinition( + apiDefinitionId: string + ): Promise; - loadAPIDefinitions(apiDefinitionIds: string[]): Promise>; + loadAPIDefinitions( + apiDefinitionIds: string[] + ): Promise>; } export class APIDefinitionDaoImpl implements APIDefinitionDao { - constructor(private readonly prisma: PrismaClient) {} + constructor(private readonly prisma: PrismaClient) {} - public async getOrgIdForApiDefinition(apiDefinitionId: string): Promise { - const apiDefinition = await this.prisma.apiDefinitionsV2.findFirst({ - where: { - apiDefinitionId, - }, - select: { - orgId: true, - }, - }); - return apiDefinition?.orgId; - } + public async getOrgIdForApiDefinition( + apiDefinitionId: string + ): Promise { + const apiDefinition = await this.prisma.apiDefinitionsV2.findFirst({ + where: { + apiDefinitionId, + }, + select: { + orgId: true, + }, + }); + return apiDefinition?.orgId; + } - public async loadAPIDefinition(apiDefinitionId: string): Promise { - const apiDefinition = await this.prisma.apiDefinitionsV2.findFirst({ - where: { - apiDefinitionId, - }, - select: { - definition: true, - }, - }); - if (apiDefinition == null) { - return undefined; - } - return readBuffer(apiDefinition.definition) as APIV1Db.DbApiDefinition; + public async loadAPIDefinition( + apiDefinitionId: string + ): Promise { + const apiDefinition = await this.prisma.apiDefinitionsV2.findFirst({ + where: { + apiDefinitionId, + }, + select: { + definition: true, + }, + }); + if (apiDefinition == null) { + return undefined; } + return readBuffer(apiDefinition.definition) as APIV1Db.DbApiDefinition; + } - public async loadAPIDefinitions(apiDefinitionIds: string[]): Promise> { - const apiDefinitions = await this.prisma.apiDefinitionsV2.findMany({ - where: { - apiDefinitionId: { - in: Array.from(apiDefinitionIds), - }, - }, - select: { - apiDefinitionId: true, - definition: true, - }, - }); - return Object.fromEntries( - apiDefinitions.map((apiDefinition) => { - return [apiDefinition.apiDefinitionId, readBuffer(apiDefinition.definition) as APIV1Db.DbApiDefinition]; - }), - ); - } + public async loadAPIDefinitions( + apiDefinitionIds: string[] + ): Promise> { + const apiDefinitions = await this.prisma.apiDefinitionsV2.findMany({ + where: { + apiDefinitionId: { + in: Array.from(apiDefinitionIds), + }, + }, + select: { + apiDefinitionId: true, + definition: true, + }, + }); + return Object.fromEntries( + apiDefinitions.map((apiDefinition) => { + return [ + apiDefinition.apiDefinitionId, + readBuffer(apiDefinition.definition) as APIV1Db.DbApiDefinition, + ]; + }) + ); + } } diff --git a/servers/fdr/src/db/docs/DocsV2Dao.ts b/servers/fdr/src/db/docs/DocsV2Dao.ts index e67a4ef8d7..36c26500ca 100644 --- a/servers/fdr/src/db/docs/DocsV2Dao.ts +++ b/servers/fdr/src/db/docs/DocsV2Dao.ts @@ -1,4 +1,11 @@ -import { APIV1Db, Algolia, DocsV1Db, DocsV2Read, FdrAPI, migrateDocsDbDefinition } from "@fern-api/fdr-sdk"; +import { + APIV1Db, + Algolia, + DocsV1Db, + DocsV2Read, + FdrAPI, + migrateDocsDbDefinition, +} from "@fern-api/fdr-sdk"; import { AuthType, PrismaClient } from "@prisma/client"; import urljoin from "url-join"; import { v4 as uuidv4 } from "uuid"; @@ -6,447 +13,499 @@ import { DocsRegistrationInfo } from "../../controllers/docs/v2/getDocsWriteV2Se import type { IndexSegment } from "../../services/algolia"; import { WithoutQuestionMarks, readBuffer, writeBuffer } from "../../util"; import { ParsedBaseUrl } from "../../util/ParsedBaseUrl"; -import { IndexSegmentIds, PrismaTransaction, ReferencedAPIDefinitionIds } from "../types"; +import { + IndexSegmentIds, + PrismaTransaction, + ReferencedAPIDefinitionIds, +} from "../types"; export interface StoreDocsDefinitionResponse { - // previousAlogliaIndex?: string; - docsDefinitionId: string; - domains: ParsedBaseUrl[]; + // previousAlogliaIndex?: string; + docsDefinitionId: string; + domains: ParsedBaseUrl[]; } export interface LoadDocsDefinitionByUrlResponse { - orgId: FdrAPI.OrgId; - domain: string; - path: string; - algoliaIndex: Algolia.AlgoliaSearchIndex | undefined; - docsDefinition: WithoutQuestionMarks; - indexSegmentIds: string[]; - docsConfigInstanceId: APIV1Db.DocsConfigId | null; - updatedTime: Date; - authType: AuthType; - hasPublicS3Assets: boolean; - isPreview: boolean; + orgId: FdrAPI.OrgId; + domain: string; + path: string; + algoliaIndex: Algolia.AlgoliaSearchIndex | undefined; + docsDefinition: WithoutQuestionMarks; + indexSegmentIds: string[]; + docsConfigInstanceId: APIV1Db.DocsConfigId | null; + updatedTime: Date; + authType: AuthType; + hasPublicS3Assets: boolean; + isPreview: boolean; } export interface LoadDocsMetadata { - orgId: FdrAPI.OrgId; - domain: string; - path: string; - isPreview: boolean; + orgId: FdrAPI.OrgId; + domain: string; + path: string; + isPreview: boolean; } export interface LoadDocsConfigResponse { - docsConfig: DocsV1Db.DocsDbConfig; - referencedApis: string[]; + docsConfig: DocsV1Db.DocsDbConfig; + referencedApis: string[]; } export interface CheckDomainOwnershipResponse { - allDomainsOwned: boolean; - unownedDomains: string[]; + allDomainsOwned: boolean; + unownedDomains: string[]; } export interface DocsV2Dao { - checkDomainsDontBelongToAnotherOrg(domains: string[], orgId: string): Promise; - - loadDocsForURL(url: URL): Promise; - - getOrgIdForDocsUrl(url: URL): Promise; - - getOrgIdForDocsConfigInstanceId(docsConfigInstanceId: string): Promise; - - loadDocsConfigByInstanceId(docsConfigInstanceId: string): Promise; - - loadDocsMetadata(url: URL): Promise; - - storeDocsDefinition({ - docsRegistrationInfo, - dbDocsDefinition, - indexSegments, - }: { - docsRegistrationInfo: DocsRegistrationInfo; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise; - - replaceDocsDefinition({ - instanceId, - dbDocsDefinition, - indexSegments, - }: { - instanceId: string; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise; - - listAllDocsUrls(opts: { - limit?: number; - page?: number; - customOnly?: boolean; - domainSuffix: string; - }): Promise; - - transferDomainOwner({ domain, toOrgId }: { domain: string; toOrgId: string }): Promise; + checkDomainsDontBelongToAnotherOrg( + domains: string[], + orgId: string + ): Promise; + + loadDocsForURL( + url: URL + ): Promise; + + getOrgIdForDocsUrl(url: URL): Promise; + + getOrgIdForDocsConfigInstanceId( + docsConfigInstanceId: string + ): Promise; + + loadDocsConfigByInstanceId( + docsConfigInstanceId: string + ): Promise; + + loadDocsMetadata(url: URL): Promise; + + storeDocsDefinition({ + docsRegistrationInfo, + dbDocsDefinition, + indexSegments, + }: { + docsRegistrationInfo: DocsRegistrationInfo; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise; + + replaceDocsDefinition({ + instanceId, + dbDocsDefinition, + indexSegments, + }: { + instanceId: string; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise; + + listAllDocsUrls(opts: { + limit?: number; + page?: number; + customOnly?: boolean; + domainSuffix: string; + }): Promise; + + transferDomainOwner({ + domain, + toOrgId, + }: { + domain: string; + toOrgId: string; + }): Promise; } export class DocsV2DaoImpl implements DocsV2Dao { - constructor(private readonly prisma: PrismaClient) {} - - public async transferDomainOwner({ domain, toOrgId }: { domain: string; toOrgId: string }): Promise { - await this.prisma.docsV2.updateMany({ - where: { - domain, - }, - data: { - orgID: toOrgId, - }, - }); - } + constructor(private readonly prisma: PrismaClient) {} - public async checkDomainsDontBelongToAnotherOrg( - domains: string[], - orgId: string, - ): Promise { - const matchedDomains = await this.prisma.docsV2.findMany({ - select: { - orgID: true, - domain: true, - }, - where: { - domain: { - in: domains, - }, - }, - distinct: ["orgID", "domain"], - }); - - const allDomainsOwned = matchedDomains.every((matchedDomain) => matchedDomain.orgID === orgId); - const unownedDomains = matchedDomains - .filter((matchedDomain) => matchedDomain.orgID !== orgId) - .map((matchedDomain) => matchedDomain.domain); - return { - allDomainsOwned, - unownedDomains, - }; - } - - public async loadDocsMetadata(url: URL): Promise { - const docsDomain = await this.prisma.docsV2.findFirst({ - where: { - domain: url.hostname, - }, - orderBy: { - updatedTime: "desc", - }, - select: { - orgID: true, - isPreview: true, - domain: true, - path: true, - }, - }); - - if (docsDomain == null) { - return undefined; - } - - return { - orgId: FdrAPI.OrgId(docsDomain.orgID), - domain: docsDomain.domain, - path: docsDomain.path, - isPreview: docsDomain.isPreview, - }; - } + public async transferDomainOwner({ + domain, + toOrgId, + }: { + domain: string; + toOrgId: string; + }): Promise { + await this.prisma.docsV2.updateMany({ + where: { + domain, + }, + data: { + orgID: toOrgId, + }, + }); + } + + public async checkDomainsDontBelongToAnotherOrg( + domains: string[], + orgId: string + ): Promise { + const matchedDomains = await this.prisma.docsV2.findMany({ + select: { + orgID: true, + domain: true, + }, + where: { + domain: { + in: domains, + }, + }, + distinct: ["orgID", "domain"], + }); - public async loadDocsForURL(url: URL): Promise | undefined> { - const docsDomain = await this.prisma.docsV2.findFirst({ - where: { - domain: url.hostname, - }, - orderBy: { - updatedTime: "desc", // first item is the latest - }, - }); - if (docsDomain == null) { - return undefined; - } - return { - algoliaIndex: - docsDomain.algoliaIndex != null ? Algolia.AlgoliaSearchIndex(docsDomain.algoliaIndex) : undefined, - orgId: FdrAPI.OrgId(docsDomain.orgID), - docsDefinition: migrateDocsDbDefinition(readBuffer(docsDomain.docsDefinition)), - docsConfigInstanceId: - docsDomain.docsConfigInstanceId != null ? APIV1Db.DocsConfigId(docsDomain.docsConfigInstanceId) : null, - indexSegmentIds: docsDomain.indexSegmentIds as IndexSegmentIds, - path: docsDomain.path, - domain: docsDomain.domain, - updatedTime: docsDomain.updatedTime, - authType: docsDomain.authType, - hasPublicS3Assets: docsDomain.hasPublicS3Assets, - isPreview: docsDomain.isPreview, - }; - } + const allDomainsOwned = matchedDomains.every( + (matchedDomain) => matchedDomain.orgID === orgId + ); + const unownedDomains = matchedDomains + .filter((matchedDomain) => matchedDomain.orgID !== orgId) + .map((matchedDomain) => matchedDomain.domain); + return { + allDomainsOwned, + unownedDomains, + }; + } + + public async loadDocsMetadata( + url: URL + ): Promise { + const docsDomain = await this.prisma.docsV2.findFirst({ + where: { + domain: url.hostname, + }, + orderBy: { + updatedTime: "desc", + }, + select: { + orgID: true, + isPreview: true, + domain: true, + path: true, + }, + }); - public async getOrgIdForDocsUrl(url: URL): Promise { - const docsDomain = await this.prisma.docsV2.findFirst({ - where: { - domain: url.hostname, - }, - select: { - orgID: true, - }, - }); - return docsDomain?.orgID != null ? FdrAPI.OrgId(docsDomain.orgID) : undefined; + if (docsDomain == null) { + return undefined; } - public async getOrgIdForDocsConfigInstanceId(docsConfigInstanceId: string): Promise { - const instance = await this.prisma.docsV2.findFirst({ - where: { - docsConfigInstanceId, - }, - select: { - orgID: true, - }, - }); - return instance?.orgID != null ? FdrAPI.OrgId(instance.orgID) : undefined; + return { + orgId: FdrAPI.OrgId(docsDomain.orgID), + domain: docsDomain.domain, + path: docsDomain.path, + isPreview: docsDomain.isPreview, + }; + } + + public async loadDocsForURL( + url: URL + ): Promise< + WithoutQuestionMarks | undefined + > { + const docsDomain = await this.prisma.docsV2.findFirst({ + where: { + domain: url.hostname, + }, + orderBy: { + updatedTime: "desc", // first item is the latest + }, + }); + if (docsDomain == null) { + return undefined; } - - public async loadDocsConfigByInstanceId(docsConfigInstanceId: string): Promise { - const instance = await this.prisma.docsConfigInstances.findFirst({ - where: { - docsConfigInstanceId, - }, - }); - if (instance == null) { - return undefined; - } - return { - docsConfig: readBuffer(instance.docsConfig) as DocsV1Db.DocsDbConfig, - referencedApis: instance.referencedApiDefinitionIds as ReferencedAPIDefinitionIds, - }; + return { + algoliaIndex: + docsDomain.algoliaIndex != null + ? Algolia.AlgoliaSearchIndex(docsDomain.algoliaIndex) + : undefined, + orgId: FdrAPI.OrgId(docsDomain.orgID), + docsDefinition: migrateDocsDbDefinition( + readBuffer(docsDomain.docsDefinition) + ), + docsConfigInstanceId: + docsDomain.docsConfigInstanceId != null + ? APIV1Db.DocsConfigId(docsDomain.docsConfigInstanceId) + : null, + indexSegmentIds: docsDomain.indexSegmentIds as IndexSegmentIds, + path: docsDomain.path, + domain: docsDomain.domain, + updatedTime: docsDomain.updatedTime, + authType: docsDomain.authType, + hasPublicS3Assets: docsDomain.hasPublicS3Assets, + isPreview: docsDomain.isPreview, + }; + } + + public async getOrgIdForDocsUrl(url: URL): Promise { + const docsDomain = await this.prisma.docsV2.findFirst({ + where: { + domain: url.hostname, + }, + select: { + orgID: true, + }, + }); + return docsDomain?.orgID != null + ? FdrAPI.OrgId(docsDomain.orgID) + : undefined; + } + + public async getOrgIdForDocsConfigInstanceId( + docsConfigInstanceId: string + ): Promise { + const instance = await this.prisma.docsV2.findFirst({ + where: { + docsConfigInstanceId, + }, + select: { + orgID: true, + }, + }); + return instance?.orgID != null ? FdrAPI.OrgId(instance.orgID) : undefined; + } + + public async loadDocsConfigByInstanceId( + docsConfigInstanceId: string + ): Promise { + const instance = await this.prisma.docsConfigInstances.findFirst({ + where: { + docsConfigInstanceId, + }, + }); + if (instance == null) { + return undefined; } + return { + docsConfig: readBuffer(instance.docsConfig) as DocsV1Db.DocsDbConfig, + referencedApis: + instance.referencedApiDefinitionIds as ReferencedAPIDefinitionIds, + }; + } + + public async storeDocsDefinition({ + docsRegistrationInfo, + dbDocsDefinition, + indexSegments, + }: { + docsRegistrationInfo: DocsRegistrationInfo; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise { + const bufferDocsDefinition = writeBuffer(dbDocsDefinition); + + // Step 1: Create new index segments associated with docs + const indexSegmentIds = indexSegments.map((s) => s.id); + await this.prisma.indexSegment.createMany({ + data: indexSegments.map((seg) => ({ + id: seg.id, + version: seg.type === "versioned" ? seg.version.id : null, + })), + }); - public async storeDocsDefinition({ - docsRegistrationInfo, - dbDocsDefinition, - indexSegments, - }: { - docsRegistrationInfo: DocsRegistrationInfo; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise { - const bufferDocsDefinition = writeBuffer(dbDocsDefinition); - - // Step 1: Create new index segments associated with docs - const indexSegmentIds = indexSegments.map((s) => s.id); - await this.prisma.indexSegment.createMany({ - data: indexSegments.map((seg) => ({ - id: seg.id, - version: seg.type === "versioned" ? seg.version.id : null, - })), - }); - - // Step 2: Store Docs Config Instance - const instanceId = generateDocsDefinitionInstanceId(); - await this.prisma.docsConfigInstances.create({ - data: { - docsConfig: writeBuffer(dbDocsDefinition.config), - docsConfigInstanceId: instanceId, - referencedApiDefinitionIds: dbDocsDefinition.referencedApis, - }, - }); - - // Step 3: Upsert the fern docs domain + custom domain url with the docs definition + algolia index - await Promise.all( - [docsRegistrationInfo.fernUrl, ...docsRegistrationInfo.customUrls].map((url) => - createOrUpdateDocsDefinition({ - tx: this.prisma, - instanceId, - domain: url.hostname, - path: url.path ?? "", - orgId: docsRegistrationInfo.orgId, - bufferDocsDefinition, - indexSegmentIds, - isPreview: docsRegistrationInfo.isPreview, - authType: docsRegistrationInfo.authType, - }), - ), - ); - - return { - docsDefinitionId: instanceId, - domains: [docsRegistrationInfo.fernUrl, ...docsRegistrationInfo.customUrls], - }; - } + // Step 2: Store Docs Config Instance + const instanceId = generateDocsDefinitionInstanceId(); + await this.prisma.docsConfigInstances.create({ + data: { + docsConfig: writeBuffer(dbDocsDefinition.config), + docsConfigInstanceId: instanceId, + referencedApiDefinitionIds: dbDocsDefinition.referencedApis, + }, + }); - public async listAllDocsUrls({ - limit = 1000, - page = 1, - customOnly = false, - domainSuffix, - }: { - limit?: number; - page?: number; - customOnly?: boolean; - domainSuffix: string; - }): Promise { - limit = Math.min(limit, 1000); - const response = await this.prisma.docsV2.findMany({ - select: { - orgID: true, - domain: true, - path: true, - updatedTime: true, - }, - where: { - isPreview: false, - authType: "PUBLIC", - domain: customOnly ? { not: { endsWith: domainSuffix } } : undefined, - }, - distinct: "domain", - orderBy: { - updatedTime: "desc", - }, - take: limit, - skip: Math.min(limit * (page - 1), 0), - }); - - return { - urls: response.map( - (r): DocsV2Read.DocsDomainItem => ({ - domain: r.domain, - basePath: r.path.length > 1 ? r.path : undefined, - organizationId: FdrAPI.OrgId(r.orgID), - updatedAt: r.updatedTime.toISOString(), - }), - ), - }; - } + // Step 3: Upsert the fern docs domain + custom domain url with the docs definition + algolia index + await Promise.all( + [docsRegistrationInfo.fernUrl, ...docsRegistrationInfo.customUrls].map( + (url) => + createOrUpdateDocsDefinition({ + tx: this.prisma, + instanceId, + domain: url.hostname, + path: url.path ?? "", + orgId: docsRegistrationInfo.orgId, + bufferDocsDefinition, + indexSegmentIds, + isPreview: docsRegistrationInfo.isPreview, + authType: docsRegistrationInfo.authType, + }) + ) + ); + + return { + docsDefinitionId: instanceId, + domains: [ + docsRegistrationInfo.fernUrl, + ...docsRegistrationInfo.customUrls, + ], + }; + } + + public async listAllDocsUrls({ + limit = 1000, + page = 1, + customOnly = false, + domainSuffix, + }: { + limit?: number; + page?: number; + customOnly?: boolean; + domainSuffix: string; + }): Promise { + limit = Math.min(limit, 1000); + const response = await this.prisma.docsV2.findMany({ + select: { + orgID: true, + domain: true, + path: true, + updatedTime: true, + }, + where: { + isPreview: false, + authType: "PUBLIC", + domain: customOnly ? { not: { endsWith: domainSuffix } } : undefined, + }, + distinct: "domain", + orderBy: { + updatedTime: "desc", + }, + take: limit, + skip: Math.min(limit * (page - 1), 0), + }); - async replaceDocsDefinition({ - instanceId, - dbDocsDefinition, - indexSegments, - }: { - instanceId: string; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise { - return this.prisma.$transaction(async (tx) => { - const bufferDocsDefinition = writeBuffer(dbDocsDefinition); - - // Step 1: Load Previous Docs - const previousDocs = await tx.docsV2.findMany({ - where: { - docsConfigInstanceId: instanceId, - }, - select: { - domain: true, - path: true, - orgID: true, - isPreview: true, - authType: true, - }, - orderBy: { - updatedTime: "desc", - }, - }); - - // Step 2: Create new index segments associated with docs - const indexSegmentIds = indexSegments.map((s) => s.id); - await tx.indexSegment.createMany({ - data: indexSegments.map((seg) => ({ - id: seg.id, - version: seg.type === "versioned" ? seg.version.id : null, - })), - }); - - // Step 3: Store Docs Config Instance - await tx.docsConfigInstances.update({ - where: { - docsConfigInstanceId: instanceId, - }, - data: { - docsConfig: writeBuffer(dbDocsDefinition.config), - referencedApiDefinitionIds: dbDocsDefinition.referencedApis, - }, - }); - - // Step 4: Upsert the fern docs domain + custom domain url with the docs definition + algolia index - await Promise.all( - previousDocs.map((previousDoc) => - createOrUpdateDocsDefinition({ - tx, - instanceId, - domain: previousDoc.domain, - path: previousDoc.path, - orgId: previousDoc.orgID, - bufferDocsDefinition, - indexSegmentIds, - isPreview: previousDoc.isPreview, - authType: previousDoc.authType, - }), - ), - ); - - return { - docsDefinitionId: instanceId, - domains: previousDocs.map((doc) => ParsedBaseUrl.parse(urljoin(doc.domain, doc.path))), - }; - }); - } + return { + urls: response.map( + (r): DocsV2Read.DocsDomainItem => ({ + domain: r.domain, + basePath: r.path.length > 1 ? r.path : undefined, + organizationId: FdrAPI.OrgId(r.orgID), + updatedAt: r.updatedTime.toISOString(), + }) + ), + }; + } + + async replaceDocsDefinition({ + instanceId, + dbDocsDefinition, + indexSegments, + }: { + instanceId: string; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise { + return this.prisma.$transaction(async (tx) => { + const bufferDocsDefinition = writeBuffer(dbDocsDefinition); + + // Step 1: Load Previous Docs + const previousDocs = await tx.docsV2.findMany({ + where: { + docsConfigInstanceId: instanceId, + }, + select: { + domain: true, + path: true, + orgID: true, + isPreview: true, + authType: true, + }, + orderBy: { + updatedTime: "desc", + }, + }); + + // Step 2: Create new index segments associated with docs + const indexSegmentIds = indexSegments.map((s) => s.id); + await tx.indexSegment.createMany({ + data: indexSegments.map((seg) => ({ + id: seg.id, + version: seg.type === "versioned" ? seg.version.id : null, + })), + }); + + // Step 3: Store Docs Config Instance + await tx.docsConfigInstances.update({ + where: { + docsConfigInstanceId: instanceId, + }, + data: { + docsConfig: writeBuffer(dbDocsDefinition.config), + referencedApiDefinitionIds: dbDocsDefinition.referencedApis, + }, + }); + + // Step 4: Upsert the fern docs domain + custom domain url with the docs definition + algolia index + await Promise.all( + previousDocs.map((previousDoc) => + createOrUpdateDocsDefinition({ + tx, + instanceId, + domain: previousDoc.domain, + path: previousDoc.path, + orgId: previousDoc.orgID, + bufferDocsDefinition, + indexSegmentIds, + isPreview: previousDoc.isPreview, + authType: previousDoc.authType, + }) + ) + ); + + return { + docsDefinitionId: instanceId, + domains: previousDocs.map((doc) => + ParsedBaseUrl.parse(urljoin(doc.domain, doc.path)) + ), + }; + }); + } } function generateDocsDefinitionInstanceId(): string { - return "docs_definition_" + uuidv4(); + return "docs_definition_" + uuidv4(); } async function createOrUpdateDocsDefinition({ - tx, - instanceId, - bufferDocsDefinition, - domain, - path, - orgId, - indexSegmentIds, - isPreview, - authType, + tx, + instanceId, + bufferDocsDefinition, + domain, + path, + orgId, + indexSegmentIds, + isPreview, + authType, }: { - tx: PrismaTransaction; - instanceId: string; - bufferDocsDefinition: Buffer; - domain: string; - path: string; - orgId: string; - indexSegmentIds: IndexSegmentIds; - isPreview: boolean; - authType: AuthType; + tx: PrismaTransaction; + instanceId: string; + bufferDocsDefinition: Buffer; + domain: string; + path: string; + orgId: string; + indexSegmentIds: IndexSegmentIds; + isPreview: boolean; + authType: AuthType; }): Promise { - await tx.docsV2.upsert({ - where: { - domain_path: { - domain, - path, - }, - }, - create: { - docsDefinition: bufferDocsDefinition, - domain, - path, - orgID: orgId, - docsConfigInstanceId: instanceId, - algoliaIndex: null, - isPreview, - authType, - hasPublicS3Assets: authType === "PUBLIC", - }, - update: { - docsDefinition: bufferDocsDefinition, - orgID: orgId, - docsConfigInstanceId: instanceId, - indexSegmentIds, - isPreview, - authType, - hasPublicS3Assets: authType === "PUBLIC", - }, - }); + await tx.docsV2.upsert({ + where: { + domain_path: { + domain, + path, + }, + }, + create: { + docsDefinition: bufferDocsDefinition, + domain, + path, + orgID: orgId, + docsConfigInstanceId: instanceId, + algoliaIndex: null, + isPreview, + authType, + hasPublicS3Assets: authType === "PUBLIC", + }, + update: { + docsDefinition: bufferDocsDefinition, + orgID: orgId, + docsConfigInstanceId: instanceId, + indexSegmentIds, + isPreview, + authType, + hasPublicS3Assets: authType === "PUBLIC", + }, + }); } diff --git a/servers/fdr/src/db/docs/IndexSegmentDao.ts b/servers/fdr/src/db/docs/IndexSegmentDao.ts index 6c6284412f..dd72f447ce 100644 --- a/servers/fdr/src/db/docs/IndexSegmentDao.ts +++ b/servers/fdr/src/db/docs/IndexSegmentDao.ts @@ -1,28 +1,32 @@ import { PrismaClient } from "@prisma/client"; export interface LoadIndexSegmentResponse { - id: string; - createdAt: Date; - version: string | undefined; + id: string; + createdAt: Date; + version: string | undefined; } export interface IndexSegmentDao { - loadIndexSegment(indexSegmentId: string): Promise; + loadIndexSegment( + indexSegmentId: string + ): Promise; } export class IndexSegmentDaoImpl implements IndexSegmentDao { - constructor(private readonly prisma: PrismaClient) {} + constructor(private readonly prisma: PrismaClient) {} - public async loadIndexSegment(indexSegmentId: string): Promise { - const record = await this.prisma.indexSegment.findFirst({ - where: { id: indexSegmentId }, - }); - return record != null - ? { - id: record.id, - createdAt: record.createdAt, - version: record.version ?? undefined, - } - : undefined; - } + public async loadIndexSegment( + indexSegmentId: string + ): Promise { + const record = await this.prisma.indexSegment.findFirst({ + where: { id: indexSegmentId }, + }); + return record != null + ? { + id: record.id, + createdAt: record.createdAt, + version: record.version ?? undefined, + } + : undefined; + } } diff --git a/servers/fdr/src/db/generators/CliVersionsDao.ts b/servers/fdr/src/db/generators/CliVersionsDao.ts index e4705f7203..41eb989cf3 100644 --- a/servers/fdr/src/db/generators/CliVersionsDao.ts +++ b/servers/fdr/src/db/generators/CliVersionsDao.ts @@ -1,212 +1,277 @@ import { APIV1Read, FdrAPI } from "@fern-api/fdr-sdk"; import * as prisma from "@prisma/client"; import { - ChangelogEntry, - ChangelogResponse, - CliRelease, - CliReleaseRequest, - GetChangelogRequest, - GetChangelogResponse, - GetLatestCliReleaseRequest, - ListCliReleasesResponse, - Yank, + ChangelogEntry, + ChangelogResponse, + CliRelease, + CliReleaseRequest, + GetChangelogRequest, + GetChangelogResponse, + GetLatestCliReleaseRequest, + ListCliReleasesResponse, + Yank, } from "../../api/generated/api/resources/generators"; import { readBuffer, writeBuffer } from "../../util"; import { - convertGeneratorReleaseType, - convertPrismaReleaseType, - getPrereleaseType, - parseSemverOrThrow, + convertGeneratorReleaseType, + convertPrismaReleaseType, + getPrereleaseType, + parseSemverOrThrow, } from "./daoUtils"; import { noncifySemanticVersion } from "./noncifySemanticVersion"; export interface LoadSnippetAPIRequest { - orgId: string; - apiName: string; + orgId: string; + apiName: string; } export interface LoadSnippetAPIsRequest { - orgIds: string[]; - apiName: string | undefined; + orgIds: string[]; + apiName: string | undefined; } export type SnippetTemplatesByEndpoint = Record< - FdrAPI.EndpointPathLiteral, - Record + FdrAPI.EndpointPathLiteral, + Record >; -export type SnippetTemplatesByEndpointIdentifier = Record; +export type SnippetTemplatesByEndpointIdentifier = Record< + string, + APIV1Read.EndpointSnippetTemplates +>; export interface CliVersionsDao { - getLatestCliRelease({ - getLatestCliReleaseRequest, - }: { - getLatestCliReleaseRequest: GetLatestCliReleaseRequest; - }): Promise; + getLatestCliRelease({ + getLatestCliReleaseRequest, + }: { + getLatestCliReleaseRequest: GetLatestCliReleaseRequest; + }): Promise; - getChangelog({ versionRanges }: { versionRanges: GetChangelogRequest }): Promise; + getChangelog({ + versionRanges, + }: { + versionRanges: GetChangelogRequest; + }): Promise; - getMinCliForIr({ irVersion }: { irVersion: number }): Promise; + getMinCliForIr({ + irVersion, + }: { + irVersion: number; + }): Promise; - upsertCliRelease({ cliRelease }: { cliRelease: CliReleaseRequest }): Promise; + upsertCliRelease({ + cliRelease, + }: { + cliRelease: CliReleaseRequest; + }): Promise; - getCliRelease({ cliVersion }: { cliVersion: string }): Promise; + getCliRelease({ + cliVersion, + }: { + cliVersion: string; + }): Promise; - listCliReleases({ page, pageSize }: { page?: number; pageSize?: number }): Promise; + listCliReleases({ + page, + pageSize, + }: { + page?: number; + pageSize?: number; + }): Promise; } export class CliVersionsDaoImpl implements CliVersionsDao { - constructor(private readonly prisma: prisma.PrismaClient) {} - async getLatestCliRelease({ - getLatestCliReleaseRequest, - }: { - getLatestCliReleaseRequest: GetLatestCliReleaseRequest; - }): Promise { - const releaseTypes = - getLatestCliReleaseRequest.releaseTypes != null - ? getLatestCliReleaseRequest.releaseTypes.map(convertGeneratorReleaseType) - : [prisma.ReleaseType.ga]; - - const maybeRelease = await this.prisma.cliRelease.findFirst({ - where: { - releaseType: { in: releaseTypes }, - irVersion: { gte: getLatestCliReleaseRequest.irVersion }, - isYanked: null, - }, - orderBy: [ - { - nonce: "desc", - }, - ], - }); - return convertPrismaCliRelease(maybeRelease); - } + constructor(private readonly prisma: prisma.PrismaClient) {} + async getLatestCliRelease({ + getLatestCliReleaseRequest, + }: { + getLatestCliReleaseRequest: GetLatestCliReleaseRequest; + }): Promise { + const releaseTypes = + getLatestCliReleaseRequest.releaseTypes != null + ? getLatestCliReleaseRequest.releaseTypes.map( + convertGeneratorReleaseType + ) + : [prisma.ReleaseType.ga]; - async getChangelog({ versionRanges }: { versionRanges: GetChangelogRequest }): Promise { - const releases = await this.prisma.cliRelease.findMany({ - where: { - nonce: { - gte: - versionRanges.fromVersion.type == "inclusive" - ? noncifySemanticVersion(versionRanges.fromVersion.value) - : undefined, - gt: - versionRanges.fromVersion.type == "exclusive" - ? noncifySemanticVersion(versionRanges.fromVersion.value) - : undefined, - lte: - versionRanges.toVersion.type == "inclusive" - ? noncifySemanticVersion(versionRanges.toVersion.value) - : undefined, - lt: - versionRanges.toVersion.type == "exclusive" - ? noncifySemanticVersion(versionRanges.toVersion.value) - : undefined, - }, - }, - orderBy: [ - { - nonce: "desc", - }, - ], - }); + const maybeRelease = await this.prisma.cliRelease.findFirst({ + where: { + releaseType: { in: releaseTypes }, + irVersion: { gte: getLatestCliReleaseRequest.irVersion }, + isYanked: null, + }, + orderBy: [ + { + nonce: "desc", + }, + ], + }); + return convertPrismaCliRelease(maybeRelease); + } - const changelogs: ChangelogResponse[] = []; - for (const release of releases) { - if (release.changelogEntry != null) { - changelogs.push({ - version: release.version, - changelogEntry: readBuffer(release.changelogEntry) as ChangelogEntry[], - }); - } - } - return { entries: changelogs }; - } + async getChangelog({ + versionRanges, + }: { + versionRanges: GetChangelogRequest; + }): Promise { + const releases = await this.prisma.cliRelease.findMany({ + where: { + nonce: { + gte: + versionRanges.fromVersion.type == "inclusive" + ? noncifySemanticVersion(versionRanges.fromVersion.value) + : undefined, + gt: + versionRanges.fromVersion.type == "exclusive" + ? noncifySemanticVersion(versionRanges.fromVersion.value) + : undefined, + lte: + versionRanges.toVersion.type == "inclusive" + ? noncifySemanticVersion(versionRanges.toVersion.value) + : undefined, + lt: + versionRanges.toVersion.type == "exclusive" + ? noncifySemanticVersion(versionRanges.toVersion.value) + : undefined, + }, + }, + orderBy: [ + { + nonce: "desc", + }, + ], + }); - async upsertCliRelease({ cliRelease }: { cliRelease: CliReleaseRequest }): Promise { - const parsedVersion = parseSemverOrThrow(cliRelease.version); - const data = { - version: cliRelease.version, - major: parsedVersion.major, - minor: parsedVersion.minor, - patch: parsedVersion.patch, - nonce: noncifySemanticVersion(cliRelease.version), - irVersion: cliRelease.irVersion, - releaseType: convertGeneratorReleaseType(getPrereleaseType(cliRelease.version)), - changelogEntry: cliRelease.changelogEntry != null ? writeBuffer(cliRelease.changelogEntry) : null, - isYanked: cliRelease.isYanked != null ? writeBuffer(cliRelease.isYanked) : null, - createdAt: cliRelease.createdAt != null ? new Date(cliRelease.createdAt) : undefined, - tags: cliRelease.tags, - }; - - await this.prisma.cliRelease.upsert({ - where: { - version: cliRelease.version, - }, - update: data, - create: data, + const changelogs: ChangelogResponse[] = []; + for (const release of releases) { + if (release.changelogEntry != null) { + changelogs.push({ + version: release.version, + changelogEntry: readBuffer( + release.changelogEntry + ) as ChangelogEntry[], }); + } } + return { entries: changelogs }; + } - async getMinCliForIr({ irVersion }: { irVersion: number }): Promise { - const maybeRelease = await this.prisma.cliRelease.findFirst({ - where: { - irVersion, - }, - orderBy: [ - { - // Get the lowest version - nonce: "asc", - }, - ], - }); - return convertPrismaCliRelease(maybeRelease); - } + async upsertCliRelease({ + cliRelease, + }: { + cliRelease: CliReleaseRequest; + }): Promise { + const parsedVersion = parseSemverOrThrow(cliRelease.version); + const data = { + version: cliRelease.version, + major: parsedVersion.major, + minor: parsedVersion.minor, + patch: parsedVersion.patch, + nonce: noncifySemanticVersion(cliRelease.version), + irVersion: cliRelease.irVersion, + releaseType: convertGeneratorReleaseType( + getPrereleaseType(cliRelease.version) + ), + changelogEntry: + cliRelease.changelogEntry != null + ? writeBuffer(cliRelease.changelogEntry) + : null, + isYanked: + cliRelease.isYanked != null ? writeBuffer(cliRelease.isYanked) : null, + createdAt: + cliRelease.createdAt != null + ? new Date(cliRelease.createdAt) + : undefined, + tags: cliRelease.tags, + }; - async getCliRelease({ cliVersion }: { cliVersion: string }): Promise { - const maybeRelease = await this.prisma.cliRelease.findUnique({ - where: { - version: cliVersion, - }, - }); - return convertPrismaCliRelease(maybeRelease); - } + await this.prisma.cliRelease.upsert({ + where: { + version: cliRelease.version, + }, + update: data, + create: data, + }); + } - async listCliReleases({ - page = 0, - pageSize = 20, - }: { - page?: number | undefined; - pageSize?: number | undefined; - }): Promise { - const releases = await this.prisma.cliRelease.findMany({ - skip: page * pageSize, - take: pageSize, - orderBy: [ - { - nonce: "desc", - }, - ], - }); + async getMinCliForIr({ + irVersion, + }: { + irVersion: number; + }): Promise { + const maybeRelease = await this.prisma.cliRelease.findFirst({ + where: { + irVersion, + }, + orderBy: [ + { + // Get the lowest version + nonce: "asc", + }, + ], + }); + return convertPrismaCliRelease(maybeRelease); + } - return { cliReleases: releases.map(convertPrismaCliRelease).filter((g): g is CliRelease => g != null) }; - } -} + async getCliRelease({ + cliVersion, + }: { + cliVersion: string; + }): Promise { + const maybeRelease = await this.prisma.cliRelease.findUnique({ + where: { + version: cliVersion, + }, + }); + return convertPrismaCliRelease(maybeRelease); + } -function convertPrismaCliRelease(cliRelease: prisma.CliRelease | null): CliRelease | undefined { - if (cliRelease == null) { - return undefined; - } + async listCliReleases({ + page = 0, + pageSize = 20, + }: { + page?: number | undefined; + pageSize?: number | undefined; + }): Promise { + const releases = await this.prisma.cliRelease.findMany({ + skip: page * pageSize, + take: pageSize, + orderBy: [ + { + nonce: "desc", + }, + ], + }); return { - version: cliRelease.version, - tags: cliRelease.tags, - irVersion: cliRelease.irVersion, - releaseType: convertPrismaReleaseType(cliRelease.releaseType), - changelogEntry: - cliRelease.changelogEntry != null ? (readBuffer(cliRelease.changelogEntry) as ChangelogEntry[]) : undefined, - majorVersion: cliRelease.major, - isYanked: cliRelease.isYanked != null ? (readBuffer(cliRelease.isYanked) as Yank) : undefined, - createdAt: cliRelease.createdAt?.toISOString(), + cliReleases: releases + .map(convertPrismaCliRelease) + .filter((g): g is CliRelease => g != null), }; + } +} + +function convertPrismaCliRelease( + cliRelease: prisma.CliRelease | null +): CliRelease | undefined { + if (cliRelease == null) { + return undefined; + } + + return { + version: cliRelease.version, + tags: cliRelease.tags, + irVersion: cliRelease.irVersion, + releaseType: convertPrismaReleaseType(cliRelease.releaseType), + changelogEntry: + cliRelease.changelogEntry != null + ? (readBuffer(cliRelease.changelogEntry) as ChangelogEntry[]) + : undefined, + majorVersion: cliRelease.major, + isYanked: + cliRelease.isYanked != null + ? (readBuffer(cliRelease.isYanked) as Yank) + : undefined, + createdAt: cliRelease.createdAt?.toISOString(), + }; } diff --git a/servers/fdr/src/db/generators/GeneratorDao.ts b/servers/fdr/src/db/generators/GeneratorDao.ts index 51d36430e5..e7ab0c0748 100644 --- a/servers/fdr/src/db/generators/GeneratorDao.ts +++ b/servers/fdr/src/db/generators/GeneratorDao.ts @@ -1,174 +1,219 @@ import { APIV1Read, FdrAPI } from "@fern-api/fdr-sdk"; import * as prisma from "@prisma/client"; import { - Generator, - GeneratorId, - GeneratorLanguage, - GeneratorScripts, - GeneratorType, - Script, + Generator, + GeneratorId, + GeneratorLanguage, + GeneratorScripts, + GeneratorType, + Script, } from "../../api/generated/api/resources/generators"; import { assertNever, readBuffer, writeBuffer } from "../../util"; export interface LoadSnippetAPIRequest { - orgId: string; - apiName: string; + orgId: string; + apiName: string; } export interface LoadSnippetAPIsRequest { - orgIds: string[]; - apiName: string | undefined; + orgIds: string[]; + apiName: string | undefined; } export type SnippetTemplatesByEndpoint = Record< - FdrAPI.EndpointPathLiteral, - Record + FdrAPI.EndpointPathLiteral, + Record >; -export type SnippetTemplatesByEndpointIdentifier = Record; +export type SnippetTemplatesByEndpointIdentifier = Record< + string, + APIV1Read.EndpointSnippetTemplates +>; export interface GeneratorsDao { - upsertGenerator({ generator }: { generator: Generator }): Promise; - - getGenerator({ generatorId }: { generatorId: GeneratorId }): Promise; - - getGeneratorByImage({ image }: { image: string }): Promise; - - listGenerators(): Promise; - - deleteGenerator({ generatorId }: { generatorId: GeneratorId }): Promise; - deleteGenerators({ generatorIds }: { generatorIds: GeneratorId[] }): Promise; + upsertGenerator({ generator }: { generator: Generator }): Promise; + + getGenerator({ + generatorId, + }: { + generatorId: GeneratorId; + }): Promise; + + getGeneratorByImage({ + image, + }: { + image: string; + }): Promise; + + listGenerators(): Promise; + + deleteGenerator({ generatorId }: { generatorId: GeneratorId }): Promise; + deleteGenerators({ + generatorIds, + }: { + generatorIds: GeneratorId[]; + }): Promise; } export class GeneratorsDaoImpl implements GeneratorsDao { - constructor(private readonly prisma: prisma.PrismaClient) {} - - async getGeneratorByImage({ image }: { image: string }): Promise { - return convertPrismaGenerator( - await this.prisma.generator.findUnique({ - where: { - dockerImage: image, - }, - }), - ); - } - async deleteGenerators({ generatorIds }: { generatorIds: string[] }): Promise { - await this.prisma.generator.deleteMany({ - where: { - id: { in: generatorIds }, - }, - }); - } - - async deleteGenerator({ generatorId }: { generatorId: string }): Promise { - await this.prisma.generator.delete({ - where: { - id: generatorId, - }, - }); - } - - async getGenerator({ generatorId }: { generatorId: GeneratorId }): Promise { - return convertPrismaGenerator( - await this.prisma.generator.findUnique({ - where: { - id: generatorId, - }, - }), - ); - } - - async listGenerators(): Promise { - const generators = await this.prisma.generator.findMany(); - - return generators.map(convertPrismaGenerator).filter((g): g is Generator => g != null); - } - - async upsertGenerator({ generator }: { generator: Generator }): Promise { - // We always just write over the previous entry here - const data = { - id: generator.id, - displayName: generator.displayName, - generatorType: writeBuffer(generator.generatorType), - generatorLanguage: convertGeneratorLanguage(generator.generatorLanguage), - dockerImage: generator.dockerImage, - scripts: generator.scripts ? writeBuffer(generator.scripts) : undefined, - }; - await this.prisma.generator.upsert({ - where: { - id: generator.id, - }, - update: data, - create: data, - }); - } + constructor(private readonly prisma: prisma.PrismaClient) {} + + async getGeneratorByImage({ + image, + }: { + image: string; + }): Promise { + return convertPrismaGenerator( + await this.prisma.generator.findUnique({ + where: { + dockerImage: image, + }, + }) + ); + } + async deleteGenerators({ + generatorIds, + }: { + generatorIds: string[]; + }): Promise { + await this.prisma.generator.deleteMany({ + where: { + id: { in: generatorIds }, + }, + }); + } + + async deleteGenerator({ + generatorId, + }: { + generatorId: string; + }): Promise { + await this.prisma.generator.delete({ + where: { + id: generatorId, + }, + }); + } + + async getGenerator({ + generatorId, + }: { + generatorId: GeneratorId; + }): Promise { + return convertPrismaGenerator( + await this.prisma.generator.findUnique({ + where: { + id: generatorId, + }, + }) + ); + } + + async listGenerators(): Promise { + const generators = await this.prisma.generator.findMany(); + + return generators + .map(convertPrismaGenerator) + .filter((g): g is Generator => g != null); + } + + async upsertGenerator({ + generator, + }: { + generator: Generator; + }): Promise { + // We always just write over the previous entry here + const data = { + id: generator.id, + displayName: generator.displayName, + generatorType: writeBuffer(generator.generatorType), + generatorLanguage: convertGeneratorLanguage(generator.generatorLanguage), + dockerImage: generator.dockerImage, + scripts: generator.scripts ? writeBuffer(generator.scripts) : undefined, + }; + await this.prisma.generator.upsert({ + where: { + id: generator.id, + }, + update: data, + create: data, + }); + } } -function convertGeneratorLanguage(generatorLanguage: GeneratorLanguage | undefined): prisma.Language | undefined { - if (generatorLanguage == null) { - return undefined; - } - switch (generatorLanguage) { - case GeneratorLanguage.Python: - return prisma.Language.PYTHON; - case GeneratorLanguage.Typescript: - return prisma.Language.TYPESCRIPT; - case GeneratorLanguage.Java: - return prisma.Language.JAVA; - case GeneratorLanguage.Go: - return prisma.Language.GO; - case GeneratorLanguage.Csharp: - return prisma.Language.CSHARP; - case GeneratorLanguage.Ruby: - return prisma.Language.RUBY; - case GeneratorLanguage.Php: - return prisma.Language.PHP; - case GeneratorLanguage.Swift: - return prisma.Language.SWIFT; - case GeneratorLanguage.Rust: - return prisma.Language.RUST; - default: - assertNever(generatorLanguage); - } +function convertGeneratorLanguage( + generatorLanguage: GeneratorLanguage | undefined +): prisma.Language | undefined { + if (generatorLanguage == null) { + return undefined; + } + switch (generatorLanguage) { + case GeneratorLanguage.Python: + return prisma.Language.PYTHON; + case GeneratorLanguage.Typescript: + return prisma.Language.TYPESCRIPT; + case GeneratorLanguage.Java: + return prisma.Language.JAVA; + case GeneratorLanguage.Go: + return prisma.Language.GO; + case GeneratorLanguage.Csharp: + return prisma.Language.CSHARP; + case GeneratorLanguage.Ruby: + return prisma.Language.RUBY; + case GeneratorLanguage.Php: + return prisma.Language.PHP; + case GeneratorLanguage.Swift: + return prisma.Language.SWIFT; + case GeneratorLanguage.Rust: + return prisma.Language.RUST; + default: + assertNever(generatorLanguage); + } } -function convertPrismaLanguage(prismaLanguage: prisma.Language | null): GeneratorLanguage | undefined { - if (prismaLanguage == null) { - return undefined; - } - switch (prismaLanguage) { - case prisma.Language.PYTHON: - return GeneratorLanguage.Python; - case prisma.Language.TYPESCRIPT: - return GeneratorLanguage.Typescript; - case prisma.Language.JAVA: - return GeneratorLanguage.Java; - case prisma.Language.GO: - return GeneratorLanguage.Go; - case prisma.Language.CSHARP: - return GeneratorLanguage.Csharp; - case prisma.Language.RUBY: - return GeneratorLanguage.Ruby; - case prisma.Language.PHP: - return GeneratorLanguage.Php; - case prisma.Language.SWIFT: - return GeneratorLanguage.Swift; - case prisma.Language.RUST: - return GeneratorLanguage.Rust; - default: - assertNever(prismaLanguage); - } +function convertPrismaLanguage( + prismaLanguage: prisma.Language | null +): GeneratorLanguage | undefined { + if (prismaLanguage == null) { + return undefined; + } + switch (prismaLanguage) { + case prisma.Language.PYTHON: + return GeneratorLanguage.Python; + case prisma.Language.TYPESCRIPT: + return GeneratorLanguage.Typescript; + case prisma.Language.JAVA: + return GeneratorLanguage.Java; + case prisma.Language.GO: + return GeneratorLanguage.Go; + case prisma.Language.CSHARP: + return GeneratorLanguage.Csharp; + case prisma.Language.RUBY: + return GeneratorLanguage.Ruby; + case prisma.Language.PHP: + return GeneratorLanguage.Php; + case prisma.Language.SWIFT: + return GeneratorLanguage.Swift; + case prisma.Language.RUST: + return GeneratorLanguage.Rust; + default: + assertNever(prismaLanguage); + } } -function convertPrismaGenerator(generator: prisma.Generator | null): Generator | undefined { - return generator != null - ? { - id: FdrAPI.generators.GeneratorId(generator.id), - displayName: generator.displayName, - generatorType: readBuffer(generator.generatorType) as GeneratorType, - generatorLanguage: convertPrismaLanguage(generator.generatorLanguage), - dockerImage: generator.dockerImage, - scripts: generator.scripts ? (readBuffer(generator.scripts) as GeneratorScripts) : undefined, - } - : undefined; +function convertPrismaGenerator( + generator: prisma.Generator | null +): Generator | undefined { + return generator != null + ? { + id: FdrAPI.generators.GeneratorId(generator.id), + displayName: generator.displayName, + generatorType: readBuffer(generator.generatorType) as GeneratorType, + generatorLanguage: convertPrismaLanguage(generator.generatorLanguage), + dockerImage: generator.dockerImage, + scripts: generator.scripts + ? (readBuffer(generator.scripts) as GeneratorScripts) + : undefined, + } + : undefined; } diff --git a/servers/fdr/src/db/generators/GeneratorVersionsDao.ts b/servers/fdr/src/db/generators/GeneratorVersionsDao.ts index 8ded94b003..fdf0a1aab3 100644 --- a/servers/fdr/src/db/generators/GeneratorVersionsDao.ts +++ b/servers/fdr/src/db/generators/GeneratorVersionsDao.ts @@ -1,280 +1,320 @@ import { APIV1Read, FdrAPI } from "@fern-api/fdr-sdk"; import * as prisma from "@prisma/client"; import { - ChangelogEntry, - ChangelogResponse, - GeneratorId, - GeneratorRelease, - GeneratorReleaseRequest, - GetChangelogRequest, - GetChangelogResponse, - GetLatestGeneratorReleaseRequest, - ListGeneratorReleasesResponse, - Yank, + ChangelogEntry, + ChangelogResponse, + GeneratorId, + GeneratorRelease, + GeneratorReleaseRequest, + GetChangelogRequest, + GetChangelogResponse, + GetLatestGeneratorReleaseRequest, + ListGeneratorReleasesResponse, + Yank, } from "../../api/generated/api/resources/generators"; import { readBuffer, writeBuffer } from "../../util"; import { - convertGeneratorReleaseType, - convertPrismaReleaseType, - getPrereleaseType, - parseSemverOrThrow, + convertGeneratorReleaseType, + convertPrismaReleaseType, + getPrereleaseType, + parseSemverOrThrow, } from "./daoUtils"; import { noncifySemanticVersion } from "./noncifySemanticVersion"; export interface LoadSnippetAPIRequest { - orgId: string; - apiName: string; + orgId: string; + apiName: string; } export interface LoadSnippetAPIsRequest { - orgIds: string[]; - apiName: string | undefined; + orgIds: string[]; + apiName: string | undefined; } export type SnippetTemplatesByEndpoint = Record< - FdrAPI.EndpointPathLiteral, - Record + FdrAPI.EndpointPathLiteral, + Record >; -export type SnippetTemplatesByEndpointIdentifier = Record; +export type SnippetTemplatesByEndpointIdentifier = Record< + string, + APIV1Read.EndpointSnippetTemplates +>; export interface GeneratorVersionsDao { - getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest, - }: { - getLatestGeneratorReleaseRequest: GetLatestGeneratorReleaseRequest; - }): Promise; + getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest, + }: { + getLatestGeneratorReleaseRequest: GetLatestGeneratorReleaseRequest; + }): Promise; - getChangelog({ - generator, - versionRanges, - }: { - generator: GeneratorId; - versionRanges: GetChangelogRequest; - }): Promise; + getChangelog({ + generator, + versionRanges, + }: { + generator: GeneratorId; + versionRanges: GetChangelogRequest; + }): Promise; - upsertGeneratorRelease({ generatorRelease }: { generatorRelease: GeneratorReleaseRequest }): Promise; + upsertGeneratorRelease({ + generatorRelease, + }: { + generatorRelease: GeneratorReleaseRequest; + }): Promise; - getGeneratorRelease({ - generator, - version, - }: { - generator: GeneratorId; - version: string; - }): Promise; + getGeneratorRelease({ + generator, + version, + }: { + generator: GeneratorId; + version: string; + }): Promise; - listGeneratorReleases({ - generator, - page, - pageSize, - }: { - generator: GeneratorId; - page?: number; - pageSize?: number; - }): Promise; + listGeneratorReleases({ + generator, + page, + pageSize, + }: { + generator: GeneratorId; + page?: number; + pageSize?: number; + }): Promise; } export class GeneratorVersionsDaoImpl implements GeneratorVersionsDao { - constructor(private readonly prisma: prisma.PrismaClient) {} + constructor(private readonly prisma: prisma.PrismaClient) {} - async upsertGeneratorRelease({ generatorRelease }: { generatorRelease: GeneratorReleaseRequest }): Promise { - const parsedVersion = parseSemverOrThrow(generatorRelease.version); + async upsertGeneratorRelease({ + generatorRelease, + }: { + generatorRelease: GeneratorReleaseRequest; + }): Promise { + const parsedVersion = parseSemverOrThrow(generatorRelease.version); - const releaseType = convertGeneratorReleaseType(getPrereleaseType(generatorRelease.version)); - const data = { - version: generatorRelease.version, - generatorId: generatorRelease.generatorId, - irVersion: generatorRelease.irVersion, - major: parsedVersion.major, - minor: parsedVersion.minor, - patch: parsedVersion.patch, - isYanked: generatorRelease.isYanked != null ? writeBuffer(generatorRelease.isYanked) : undefined, - changelogEntry: - generatorRelease.changelogEntry != null ? writeBuffer(generatorRelease.changelogEntry) : undefined, - migration: generatorRelease.migration != null ? writeBuffer(generatorRelease.migration) : undefined, - customConfigSchema: generatorRelease.customConfigSchema, - releaseType, - nonce: noncifySemanticVersion(generatorRelease.version), - createdAt: generatorRelease.createdAt != null ? new Date(generatorRelease.createdAt) : undefined, - tags: generatorRelease.tags, - }; - await this.prisma.generatorRelease.upsert({ - where: { - generatorId_version: { - generatorId: generatorRelease.generatorId, - version: generatorRelease.version, - }, - }, - create: data, - update: data, - }); - } + const releaseType = convertGeneratorReleaseType( + getPrereleaseType(generatorRelease.version) + ); + const data = { + version: generatorRelease.version, + generatorId: generatorRelease.generatorId, + irVersion: generatorRelease.irVersion, + major: parsedVersion.major, + minor: parsedVersion.minor, + patch: parsedVersion.patch, + isYanked: + generatorRelease.isYanked != null + ? writeBuffer(generatorRelease.isYanked) + : undefined, + changelogEntry: + generatorRelease.changelogEntry != null + ? writeBuffer(generatorRelease.changelogEntry) + : undefined, + migration: + generatorRelease.migration != null + ? writeBuffer(generatorRelease.migration) + : undefined, + customConfigSchema: generatorRelease.customConfigSchema, + releaseType, + nonce: noncifySemanticVersion(generatorRelease.version), + createdAt: + generatorRelease.createdAt != null + ? new Date(generatorRelease.createdAt) + : undefined, + tags: generatorRelease.tags, + }; + await this.prisma.generatorRelease.upsert({ + where: { + generatorId_version: { + generatorId: generatorRelease.generatorId, + version: generatorRelease.version, + }, + }, + create: data, + update: data, + }); + } - async getGeneratorRelease({ - generator, - version, - }: { - generator: string; - version: string; - }): Promise { - const maybeRelease = await this.prisma.generatorRelease.findUnique({ - where: { - generatorId_version: { - generatorId: generator, - version, - }, - }, - }); - return maybeRelease != null ? convertPrismaGeneratorRelease(maybeRelease) : undefined; - } + async getGeneratorRelease({ + generator, + version, + }: { + generator: string; + version: string; + }): Promise { + const maybeRelease = await this.prisma.generatorRelease.findUnique({ + where: { + generatorId_version: { + generatorId: generator, + version, + }, + }, + }); + return maybeRelease != null + ? convertPrismaGeneratorRelease(maybeRelease) + : undefined; + } - async listGeneratorReleases({ - generator, - page = 0, - pageSize = 20, - }: { - generator: GeneratorId; - page?: number; - pageSize?: number; - }): Promise { - const releases = await this.prisma.generatorRelease.findMany({ - where: { - generatorId: generator, - }, - skip: page * pageSize, - take: pageSize, - orderBy: [ - { - nonce: "desc", - }, - ], - }); + async listGeneratorReleases({ + generator, + page = 0, + pageSize = 20, + }: { + generator: GeneratorId; + page?: number; + pageSize?: number; + }): Promise { + const releases = await this.prisma.generatorRelease.findMany({ + where: { + generatorId: generator, + }, + skip: page * pageSize, + take: pageSize, + orderBy: [ + { + nonce: "desc", + }, + ], + }); - return { - generatorReleases: releases - .map(convertPrismaGeneratorRelease) - .filter((g): g is GeneratorRelease => g != null), - }; - } + return { + generatorReleases: releases + .map(convertPrismaGeneratorRelease) + .filter((g): g is GeneratorRelease => g != null), + }; + } - async getLatestGeneratorRelease({ - getLatestGeneratorReleaseRequest, - }: { - getLatestGeneratorReleaseRequest: GetLatestGeneratorReleaseRequest; - }): Promise { - const releaseTypes = - getLatestGeneratorReleaseRequest.releaseTypes != null - ? getLatestGeneratorReleaseRequest.releaseTypes.map(convertGeneratorReleaseType) - : [prisma.ReleaseType.ga]; + async getLatestGeneratorRelease({ + getLatestGeneratorReleaseRequest, + }: { + getLatestGeneratorReleaseRequest: GetLatestGeneratorReleaseRequest; + }): Promise { + const releaseTypes = + getLatestGeneratorReleaseRequest.releaseTypes != null + ? getLatestGeneratorReleaseRequest.releaseTypes.map( + convertGeneratorReleaseType + ) + : [prisma.ReleaseType.ga]; - const release = await this.prisma.$transaction(async (prisma) => { - // If an IR version is provided outright, we can use that to filter the generators - let irVersion = getLatestGeneratorReleaseRequest.irVersion; + const release = await this.prisma.$transaction(async (prisma) => { + // If an IR version is provided outright, we can use that to filter the generators + let irVersion = getLatestGeneratorReleaseRequest.irVersion; - // If a CLI version is provided, this takes precedence over a provided IR version if both - // are provided. When a CLI version is provided we need to find the IR version that corresponds to it - // to filter the generators to compatible versions. - if (getLatestGeneratorReleaseRequest.cliVersion != null) { - const cliRelease = await prisma.cliRelease.findUnique({ - where: { - version: getLatestGeneratorReleaseRequest.cliVersion, - }, - }); + // If a CLI version is provided, this takes precedence over a provided IR version if both + // are provided. When a CLI version is provided we need to find the IR version that corresponds to it + // to filter the generators to compatible versions. + if (getLatestGeneratorReleaseRequest.cliVersion != null) { + const cliRelease = await prisma.cliRelease.findUnique({ + where: { + version: getLatestGeneratorReleaseRequest.cliVersion, + }, + }); - if (cliRelease != null) { - irVersion = cliRelease.irVersion; - } - } + if (cliRelease != null) { + irVersion = cliRelease.irVersion; + } + } - // The above sets the filter `irVersion`, and that's all. - return await prisma.generatorRelease.findFirst({ - where: { - generatorId: getLatestGeneratorReleaseRequest.generator, - releaseType: { in: releaseTypes }, - major: getLatestGeneratorReleaseRequest.generatorMajorVersion, - irVersion: { lte: irVersion }, - isYanked: null, - }, - orderBy: [ - { - nonce: "desc", - }, - ], - }); - }); + // The above sets the filter `irVersion`, and that's all. + return await prisma.generatorRelease.findFirst({ + where: { + generatorId: getLatestGeneratorReleaseRequest.generator, + releaseType: { in: releaseTypes }, + major: getLatestGeneratorReleaseRequest.generatorMajorVersion, + irVersion: { lte: irVersion }, + isYanked: null, + }, + orderBy: [ + { + nonce: "desc", + }, + ], + }); + }); - return convertPrismaGeneratorRelease(release); - } + return convertPrismaGeneratorRelease(release); + } - async getChangelog({ - generator, - versionRanges, - }: { - generator: GeneratorId; - versionRanges: GetChangelogRequest; - }): Promise { - const releases = await this.prisma.generatorRelease.findMany({ - where: { - generatorId: generator, - nonce: { - gte: - versionRanges.fromVersion.type == "inclusive" - ? noncifySemanticVersion(versionRanges.fromVersion.value) - : undefined, - gt: - versionRanges.fromVersion.type == "exclusive" - ? noncifySemanticVersion(versionRanges.fromVersion.value) - : undefined, - lte: - versionRanges.toVersion.type == "inclusive" - ? noncifySemanticVersion(versionRanges.toVersion.value) - : undefined, - lt: - versionRanges.toVersion.type == "exclusive" - ? noncifySemanticVersion(versionRanges.toVersion.value) - : undefined, - }, - }, - orderBy: [ - { - nonce: "desc", - }, - ], - }); + async getChangelog({ + generator, + versionRanges, + }: { + generator: GeneratorId; + versionRanges: GetChangelogRequest; + }): Promise { + const releases = await this.prisma.generatorRelease.findMany({ + where: { + generatorId: generator, + nonce: { + gte: + versionRanges.fromVersion.type == "inclusive" + ? noncifySemanticVersion(versionRanges.fromVersion.value) + : undefined, + gt: + versionRanges.fromVersion.type == "exclusive" + ? noncifySemanticVersion(versionRanges.fromVersion.value) + : undefined, + lte: + versionRanges.toVersion.type == "inclusive" + ? noncifySemanticVersion(versionRanges.toVersion.value) + : undefined, + lt: + versionRanges.toVersion.type == "exclusive" + ? noncifySemanticVersion(versionRanges.toVersion.value) + : undefined, + }, + }, + orderBy: [ + { + nonce: "desc", + }, + ], + }); - const changelogs: ChangelogResponse[] = []; - for (const release of releases) { - if (release.changelogEntry != null) { - changelogs.push({ - version: release.version, - changelogEntry: readBuffer(release.changelogEntry) as ChangelogEntry[], - }); - } - } - return { entries: changelogs }; + const changelogs: ChangelogResponse[] = []; + for (const release of releases) { + if (release.changelogEntry != null) { + changelogs.push({ + version: release.version, + changelogEntry: readBuffer( + release.changelogEntry + ) as ChangelogEntry[], + }); + } } + return { entries: changelogs }; + } } -function convertPrismaGeneratorRelease(generatorRelease: prisma.GeneratorRelease | null): GeneratorRelease | undefined { - if (generatorRelease == null) { - return undefined; - } +function convertPrismaGeneratorRelease( + generatorRelease: prisma.GeneratorRelease | null +): GeneratorRelease | undefined { + if (generatorRelease == null) { + return undefined; + } - return { - generatorId: FdrAPI.generators.GeneratorId(generatorRelease.generatorId), - version: generatorRelease.version, - irVersion: generatorRelease.irVersion, - releaseType: convertPrismaReleaseType(generatorRelease.releaseType), - changelogEntry: - generatorRelease.changelogEntry != null - ? (readBuffer(generatorRelease.changelogEntry) as ChangelogEntry[]) - : undefined, - migration: generatorRelease.migration != null ? (readBuffer(generatorRelease.migration) as string) : undefined, - customConfigSchema: - generatorRelease.customConfigSchema != null ? generatorRelease.customConfigSchema : undefined, - majorVersion: generatorRelease.major, - isYanked: generatorRelease.isYanked != null ? (readBuffer(generatorRelease.isYanked) as Yank) : undefined, - createdAt: generatorRelease.createdAt?.toISOString(), - tags: generatorRelease.tags, - }; + return { + generatorId: FdrAPI.generators.GeneratorId(generatorRelease.generatorId), + version: generatorRelease.version, + irVersion: generatorRelease.irVersion, + releaseType: convertPrismaReleaseType(generatorRelease.releaseType), + changelogEntry: + generatorRelease.changelogEntry != null + ? (readBuffer(generatorRelease.changelogEntry) as ChangelogEntry[]) + : undefined, + migration: + generatorRelease.migration != null + ? (readBuffer(generatorRelease.migration) as string) + : undefined, + customConfigSchema: + generatorRelease.customConfigSchema != null + ? generatorRelease.customConfigSchema + : undefined, + majorVersion: generatorRelease.major, + isYanked: + generatorRelease.isYanked != null + ? (readBuffer(generatorRelease.isYanked) as Yank) + : undefined, + createdAt: generatorRelease.createdAt?.toISOString(), + tags: generatorRelease.tags, + }; } diff --git a/servers/fdr/src/db/generators/daoUtils.ts b/servers/fdr/src/db/generators/daoUtils.ts index 3c8e62f4e2..bd60b9b5a3 100644 --- a/servers/fdr/src/db/generators/daoUtils.ts +++ b/servers/fdr/src/db/generators/daoUtils.ts @@ -1,84 +1,100 @@ import * as prisma from "@prisma/client"; import semver from "semver"; -import { InvalidVersionError, ReleaseType } from "../../api/generated/api/resources/generators"; +import { + InvalidVersionError, + ReleaseType, +} from "../../api/generated/api/resources/generators"; -export function convertGeneratorReleaseType(releaseType: ReleaseType): prisma.ReleaseType { - switch (releaseType) { - case ReleaseType.Ga: - return prisma.ReleaseType.ga; - case ReleaseType.Rc: - return prisma.ReleaseType.rc; - } +export function convertGeneratorReleaseType( + releaseType: ReleaseType +): prisma.ReleaseType { + switch (releaseType) { + case ReleaseType.Ga: + return prisma.ReleaseType.ga; + case ReleaseType.Rc: + return prisma.ReleaseType.rc; + } } -export function convertPrismaReleaseType(releaseType: prisma.ReleaseType): ReleaseType { - switch (releaseType) { - case prisma.ReleaseType.ga: - return ReleaseType.Ga; - case prisma.ReleaseType.rc: - return ReleaseType.Rc; - } +export function convertPrismaReleaseType( + releaseType: prisma.ReleaseType +): ReleaseType { + switch (releaseType) { + case prisma.ReleaseType.ga: + return ReleaseType.Ga; + case prisma.ReleaseType.rc: + return ReleaseType.Rc; + } } export function parseSemverOrThrow(version: string): semver.SemVer { - const parsedVersion = semver.parse(version); - if (parsedVersion == null) { - throw new InvalidVersionError({ providedVersion: version }); - } + const parsedVersion = semver.parse(version); + if (parsedVersion == null) { + throw new InvalidVersionError({ providedVersion: version }); + } - return parsedVersion; + return parsedVersion; } export function getPrereleaseType(version: string): ReleaseType { - return getPrereleaseTypeAndVersion(version)[0]; + return getPrereleaseTypeAndVersion(version)[0]; } -export function getPrereleaseTypeAndVersion(version: string): [ReleaseType, number] { - const parsedVersion = parseSemverOrThrow(version); - - // For convenience with the semver library, we are expecting versions to come through as: - // major.minor.patch[-prereleaseType].[prereleaseVersion] where `prereleaseVersion` - // may be omitted and assumed as 0. - if (parsedVersion.prerelease.length > 0 && parsedVersion.prerelease.length < 3) { - const prereleaseType = parsedVersion.prerelease[0]?.toString(); - if (prereleaseType == null) { - throw new InvalidVersionError({ providedVersion: version }); - } +export function getPrereleaseTypeAndVersion( + version: string +): [ReleaseType, number] { + const parsedVersion = parseSemverOrThrow(version); - // Here we have 2 match groups, one for the type and one for the version. - const prereleaseTypeWithVersion = prereleaseType.match(/((?:rc|alpha|beta))([0-9]+)/); - if ( - parsedVersion.prerelease.length === 1 && - prereleaseTypeWithVersion?.[1] != null && - prereleaseTypeWithVersion[2] != null - ) { - const truePrereleaseType = prereleaseTypeWithVersion[1]; - const prereleaseVersion = parseInt(prereleaseTypeWithVersion[2], 10); - return [getPrereleaseTypeRaw(truePrereleaseType), prereleaseVersion]; - } else { - let prereleaseVersion = 0; - if (parsedVersion.prerelease.length > 1) { - const truePrereleaseVersion = parseInt(parsedVersion.prerelease[1] as string); - if (isNaN(prereleaseVersion)) { - throw new InvalidVersionError({ providedVersion: version }); - } - prereleaseVersion = truePrereleaseVersion; - } + // For convenience with the semver library, we are expecting versions to come through as: + // major.minor.patch[-prereleaseType].[prereleaseVersion] where `prereleaseVersion` + // may be omitted and assumed as 0. + if ( + parsedVersion.prerelease.length > 0 && + parsedVersion.prerelease.length < 3 + ) { + const prereleaseType = parsedVersion.prerelease[0]?.toString(); + if (prereleaseType == null) { + throw new InvalidVersionError({ providedVersion: version }); + } - return [getPrereleaseTypeRaw(prereleaseType), prereleaseVersion]; + // Here we have 2 match groups, one for the type and one for the version. + const prereleaseTypeWithVersion = prereleaseType.match( + /((?:rc|alpha|beta))([0-9]+)/ + ); + if ( + parsedVersion.prerelease.length === 1 && + prereleaseTypeWithVersion?.[1] != null && + prereleaseTypeWithVersion[2] != null + ) { + const truePrereleaseType = prereleaseTypeWithVersion[1]; + const prereleaseVersion = parseInt(prereleaseTypeWithVersion[2], 10); + return [getPrereleaseTypeRaw(truePrereleaseType), prereleaseVersion]; + } else { + let prereleaseVersion = 0; + if (parsedVersion.prerelease.length > 1) { + const truePrereleaseVersion = parseInt( + parsedVersion.prerelease[1] as string + ); + if (isNaN(prereleaseVersion)) { + throw new InvalidVersionError({ providedVersion: version }); } - } else if (parsedVersion.prerelease.length === 0) { - return [ReleaseType.Ga, 0]; + prereleaseVersion = truePrereleaseVersion; + } + + return [getPrereleaseTypeRaw(prereleaseType), prereleaseVersion]; } + } else if (parsedVersion.prerelease.length === 0) { + return [ReleaseType.Ga, 0]; + } - throw new InvalidVersionError({ providedVersion: version }); + throw new InvalidVersionError({ providedVersion: version }); } function getPrereleaseTypeRaw(version: string): ReleaseType { - switch (version) { - case "rc": - return ReleaseType.Rc; - default: - throw new InvalidVersionError({ providedVersion: version }); - } + switch (version) { + case "rc": + return ReleaseType.Rc; + default: + throw new InvalidVersionError({ providedVersion: version }); + } } diff --git a/servers/fdr/src/db/generators/noncifySemanticVersion.ts b/servers/fdr/src/db/generators/noncifySemanticVersion.ts index fb30d545c7..9b056e15e1 100644 --- a/servers/fdr/src/db/generators/noncifySemanticVersion.ts +++ b/servers/fdr/src/db/generators/noncifySemanticVersion.ts @@ -4,25 +4,26 @@ import { getPrereleaseTypeAndVersion, parseSemverOrThrow } from "./daoUtils"; const NONCE_PIECE_LENGTH = 5; export function noncifySemanticVersion(version: string): string { - const parsedVersion = parseSemverOrThrow(version); + const parsedVersion = parseSemverOrThrow(version); - let prereleaseNonceIndicator = "15"; // Indicative of a non-prerelease version + let prereleaseNonceIndicator = "15"; // Indicative of a non-prerelease version - const [prereleaseType, prereleaseVersion] = getPrereleaseTypeAndVersion(version); - switch (prereleaseType) { - case ReleaseType.Rc: - prereleaseNonceIndicator = "12"; - break; - case ReleaseType.Ga: - prereleaseNonceIndicator = "15"; - break; - default: - assertNever(prereleaseType); - } + const [prereleaseType, prereleaseVersion] = + getPrereleaseTypeAndVersion(version); + switch (prereleaseType) { + case ReleaseType.Rc: + prereleaseNonceIndicator = "12"; + break; + case ReleaseType.Ga: + prereleaseNonceIndicator = "15"; + break; + default: + assertNever(prereleaseType); + } - return `${pad(parsedVersion.major)}-${pad(parsedVersion.minor)}-${pad(parsedVersion.patch)}-${prereleaseNonceIndicator}-${pad(prereleaseVersion)}`; + return `${pad(parsedVersion.major)}-${pad(parsedVersion.minor)}-${pad(parsedVersion.patch)}-${prereleaseNonceIndicator}-${pad(prereleaseVersion)}`; } function pad(word: number): string { - return word.toString().padStart(NONCE_PIECE_LENGTH, "0"); + return word.toString().padStart(NONCE_PIECE_LENGTH, "0"); } diff --git a/servers/fdr/src/db/git/GitDao.ts b/servers/fdr/src/db/git/GitDao.ts index 013a6f0f1d..2f3e18565b 100644 --- a/servers/fdr/src/db/git/GitDao.ts +++ b/servers/fdr/src/db/git/GitDao.ts @@ -1,358 +1,394 @@ import { APIV1Read, FdrAPI } from "@fern-api/fdr-sdk"; import * as prisma from "@prisma/client"; import { - CheckRun, - FernRepository, - GithubUser, - ListPullRequestsResponse, - ListRepositoriesResponse, - PullRequest, - PullRequestReviewer, - PullRequestState, + CheckRun, + FernRepository, + GithubUser, + ListPullRequestsResponse, + ListRepositoriesResponse, + PullRequest, + PullRequestReviewer, + PullRequestState, } from "../../api/generated/api"; import { readBuffer, writeBuffer } from "../../util"; export interface LoadSnippetAPIRequest { - orgId: string; - apiName: string; + orgId: string; + apiName: string; } export interface LoadSnippetAPIsRequest { - orgIds: string[]; - apiName: string | undefined; + orgIds: string[]; + apiName: string | undefined; } export type SnippetTemplatesByEndpoint = Record< - FdrAPI.EndpointPathLiteral, - Record + FdrAPI.EndpointPathLiteral, + Record >; -export type SnippetTemplatesByEndpointIdentifier = Record; +export type SnippetTemplatesByEndpointIdentifier = Record< + string, + APIV1Read.EndpointSnippetTemplates +>; export interface GitDao { - getRepository({ - repositoryOwner, - repositoryName, - }: { - repositoryOwner: string; - repositoryName: string; - }): Promise; + getRepository({ + repositoryOwner, + repositoryName, + }: { + repositoryOwner: string; + repositoryName: string; + }): Promise; - listRepository({ - page, - pageSize, - repositoryOwner, - repositoryName, - organizationId, - }: { - page?: number | undefined; - pageSize?: number | undefined; - repositoryOwner: string | undefined; - repositoryName: string | undefined; - organizationId: string | undefined; - }): Promise; + listRepository({ + page, + pageSize, + repositoryOwner, + repositoryName, + organizationId, + }: { + page?: number | undefined; + pageSize?: number | undefined; + repositoryOwner: string | undefined; + repositoryName: string | undefined; + organizationId: string | undefined; + }): Promise; - upsertRepository({ repository }: { repository: FernRepository }): Promise; + upsertRepository({ + repository, + }: { + repository: FernRepository; + }): Promise; - deleteRepository({ - repositoryOwner, - repositoryName, - }: { - repositoryOwner: string; - repositoryName: string; - }): Promise; + deleteRepository({ + repositoryOwner, + repositoryName, + }: { + repositoryOwner: string; + repositoryName: string; + }): Promise; - getPullRequest({ - repositoryOwner, - repositoryName, - pullRequestNumber, - }: { - repositoryOwner: string; - repositoryName: string; - pullRequestNumber: number; - }): Promise; + getPullRequest({ + repositoryOwner, + repositoryName, + pullRequestNumber, + }: { + repositoryOwner: string; + repositoryName: string; + pullRequestNumber: number; + }): Promise; - listPullRequests({ - page, - pageSize, - repositoryOwner, - repositoryName, - organizationId, - state, - author, - }: { - page?: number | undefined; - pageSize?: number | undefined; - repositoryOwner: string | undefined; - repositoryName: string | undefined; - organizationId: string | undefined; - state: PullRequestState[] | undefined; - author: string[] | undefined; - }): Promise; + listPullRequests({ + page, + pageSize, + repositoryOwner, + repositoryName, + organizationId, + state, + author, + }: { + page?: number | undefined; + pageSize?: number | undefined; + repositoryOwner: string | undefined; + repositoryName: string | undefined; + organizationId: string | undefined; + state: PullRequestState[] | undefined; + author: string[] | undefined; + }): Promise; - upsertPullRequest({ pullRequest }: { pullRequest: PullRequest }): Promise; + upsertPullRequest({ + pullRequest, + }: { + pullRequest: PullRequest; + }): Promise; - deletePullRequest({ - repositoryOwner, - repositoryName, - pullRequestNumber, - }: { - repositoryOwner: string; - repositoryName: string; - pullRequestNumber: number; - }): Promise; + deletePullRequest({ + repositoryOwner, + repositoryName, + pullRequestNumber, + }: { + repositoryOwner: string; + repositoryName: string; + pullRequestNumber: number; + }): Promise; } export class GitDaoImpl implements GitDao { - constructor(private readonly prisma: prisma.PrismaClient) {} - async listPullRequests({ - page = 0, - pageSize = 20, - repositoryOwner, - repositoryName, - organizationId, - state, - author, - }: { - page?: number | undefined; - pageSize?: number | undefined; - repositoryOwner: string | undefined; - repositoryName: string | undefined; - organizationId: string | undefined; - state: PullRequestState[] | undefined; - author: string[] | undefined; - }): Promise { - const where: Record = {}; - if (repositoryOwner != null) { - where.repositoryOwner = repositoryOwner; - } - if (repositoryName != null) { - where.repositoryName = repositoryName; - } - if (organizationId != null) { - where.repository = { - repositoryOwnerOrganizationId: organizationId, - }; - } - if (state != null) { - where.state = { - in: state, - }; - } - if (author != null) { - where.authorLogin = { - in: author, - }; - } - const pull = await this.prisma.pullRequest.findMany({ - skip: page * pageSize, - take: pageSize, - where, - orderBy: { - createdAt: "desc", - }, - }); - - return { pullRequests: pull.map(convertPrismaPullRequest).filter((g): g is PullRequest => g != null) }; + constructor(private readonly prisma: prisma.PrismaClient) {} + async listPullRequests({ + page = 0, + pageSize = 20, + repositoryOwner, + repositoryName, + organizationId, + state, + author, + }: { + page?: number | undefined; + pageSize?: number | undefined; + repositoryOwner: string | undefined; + repositoryName: string | undefined; + organizationId: string | undefined; + state: PullRequestState[] | undefined; + author: string[] | undefined; + }): Promise { + const where: Record = {}; + if (repositoryOwner != null) { + where.repositoryOwner = repositoryOwner; + } + if (repositoryName != null) { + where.repositoryName = repositoryName; + } + if (organizationId != null) { + where.repository = { + repositoryOwnerOrganizationId: organizationId, + }; } + if (state != null) { + where.state = { + in: state, + }; + } + if (author != null) { + where.authorLogin = { + in: author, + }; + } + const pull = await this.prisma.pullRequest.findMany({ + skip: page * pageSize, + take: pageSize, + where, + orderBy: { + createdAt: "desc", + }, + }); - async upsertPullRequest({ pullRequest }: { pullRequest: PullRequest }): Promise { - const data: prisma.PullRequest = { - pullRequestNumber: pullRequest.pullRequestNumber, - repositoryOwner: pullRequest.repositoryOwner, - repositoryName: pullRequest.repositoryName, - author: writeBuffer(pullRequest.author), - authorLogin: pullRequest.author?.username ? pullRequest.author?.username : null, - reviewers: writeBuffer(pullRequest.reviewers), - checks: writeBuffer(pullRequest.checks), - title: pullRequest.title, - url: pullRequest.url, - state: pullRequest.state, - createdAt: new Date(pullRequest.createdAt), - updatedAt: pullRequest.updatedAt ? new Date(pullRequest.updatedAt) : null, - closedAt: pullRequest.closedAt ? new Date(pullRequest.closedAt) : null, - mergedAt: pullRequest.mergedAt ? new Date(pullRequest.mergedAt) : null, - }; + return { + pullRequests: pull + .map(convertPrismaPullRequest) + .filter((g): g is PullRequest => g != null), + }; + } - await this.prisma.pullRequest.upsert({ - where: { - pullRequestNumber_repositoryOwner_repositoryName: { - pullRequestNumber: pullRequest.pullRequestNumber, - repositoryOwner: pullRequest.repositoryOwner, - repositoryName: pullRequest.repositoryName, - }, - }, - update: data, - create: data, - }); - } + async upsertPullRequest({ + pullRequest, + }: { + pullRequest: PullRequest; + }): Promise { + const data: prisma.PullRequest = { + pullRequestNumber: pullRequest.pullRequestNumber, + repositoryOwner: pullRequest.repositoryOwner, + repositoryName: pullRequest.repositoryName, + author: writeBuffer(pullRequest.author), + authorLogin: pullRequest.author?.username + ? pullRequest.author?.username + : null, + reviewers: writeBuffer(pullRequest.reviewers), + checks: writeBuffer(pullRequest.checks), + title: pullRequest.title, + url: pullRequest.url, + state: pullRequest.state, + createdAt: new Date(pullRequest.createdAt), + updatedAt: pullRequest.updatedAt ? new Date(pullRequest.updatedAt) : null, + closedAt: pullRequest.closedAt ? new Date(pullRequest.closedAt) : null, + mergedAt: pullRequest.mergedAt ? new Date(pullRequest.mergedAt) : null, + }; - async deletePullRequest({ - repositoryOwner, - repositoryName, - pullRequestNumber, - }: { - repositoryOwner: string; - repositoryName: string; - pullRequestNumber: number; - }): Promise { - await this.prisma.pullRequest.delete({ - where: { - pullRequestNumber_repositoryOwner_repositoryName: { - pullRequestNumber, - repositoryOwner, - repositoryName, - }, - }, - }); - } + await this.prisma.pullRequest.upsert({ + where: { + pullRequestNumber_repositoryOwner_repositoryName: { + pullRequestNumber: pullRequest.pullRequestNumber, + repositoryOwner: pullRequest.repositoryOwner, + repositoryName: pullRequest.repositoryName, + }, + }, + update: data, + create: data, + }); + } - async getPullRequest({ - repositoryOwner, - repositoryName, - pullRequestNumber, - }: { - repositoryOwner: string; - repositoryName: string; - pullRequestNumber: number; - }): Promise { - return convertPrismaPullRequest( - await this.prisma.pullRequest.findUnique({ - where: { - pullRequestNumber_repositoryOwner_repositoryName: { - pullRequestNumber, - repositoryOwner, - repositoryName, - }, - }, - }), - ); - } + async deletePullRequest({ + repositoryOwner, + repositoryName, + pullRequestNumber, + }: { + repositoryOwner: string; + repositoryName: string; + pullRequestNumber: number; + }): Promise { + await this.prisma.pullRequest.delete({ + where: { + pullRequestNumber_repositoryOwner_repositoryName: { + pullRequestNumber, + repositoryOwner, + repositoryName, + }, + }, + }); + } - async upsertRepository({ repository }: { repository: FernRepository }): Promise { - const data: prisma.Repository = { - id: repository.id.id, - name: repository.name, - owner: repository.owner, - fullName: repository.fullName, - url: repository.url, - repositoryOwnerOrganizationId: repository.repositoryOwnerOrganizationId, - defaultBranchChecks: writeBuffer(repository.defaultBranchChecks), - contentType: repository.type, - systemType: repository.id.type, + async getPullRequest({ + repositoryOwner, + repositoryName, + pullRequestNumber, + }: { + repositoryOwner: string; + repositoryName: string; + pullRequestNumber: number; + }): Promise { + return convertPrismaPullRequest( + await this.prisma.pullRequest.findUnique({ + where: { + pullRequestNumber_repositoryOwner_repositoryName: { + pullRequestNumber, + repositoryOwner, + repositoryName, + }, + }, + }) + ); + } - rawRepository: writeBuffer(repository), - }; + async upsertRepository({ + repository, + }: { + repository: FernRepository; + }): Promise { + const data: prisma.Repository = { + id: repository.id.id, + name: repository.name, + owner: repository.owner, + fullName: repository.fullName, + url: repository.url, + repositoryOwnerOrganizationId: repository.repositoryOwnerOrganizationId, + defaultBranchChecks: writeBuffer(repository.defaultBranchChecks), + contentType: repository.type, + systemType: repository.id.type, - await this.prisma.repository.upsert({ - where: { - owner_name: { - name: repository.name, - owner: repository.owner, - }, - }, - update: data, - create: data, - }); - } + rawRepository: writeBuffer(repository), + }; - async listRepository({ - page = 0, - pageSize = 20, - repositoryOwner, - repositoryName, - organizationId, - }: { - page?: number | undefined; - pageSize?: number | undefined; - repositoryOwner: string | undefined; - repositoryName: string | undefined; - organizationId: string | undefined; - }): Promise { - const where: Record = {}; - if (repositoryOwner != null) { - where.repositoryOwner = repositoryOwner; - } - if (repositoryName != null) { - where.repositoryName = repositoryName; - } - if (organizationId != null) { - where.repositoryOwnerOrganizationId = organizationId; - } - const repos = await this.prisma.repository.findMany({ - skip: page * pageSize, - take: pageSize, - where, - orderBy: { - fullName: "asc", - }, - }); + await this.prisma.repository.upsert({ + where: { + owner_name: { + name: repository.name, + owner: repository.owner, + }, + }, + update: data, + create: data, + }); + } - return { repositories: repos.map(convertPrismaRepo).filter((g): g is FernRepository => g != null) }; + async listRepository({ + page = 0, + pageSize = 20, + repositoryOwner, + repositoryName, + organizationId, + }: { + page?: number | undefined; + pageSize?: number | undefined; + repositoryOwner: string | undefined; + repositoryName: string | undefined; + organizationId: string | undefined; + }): Promise { + const where: Record = {}; + if (repositoryOwner != null) { + where.repositoryOwner = repositoryOwner; } - - async getRepository({ - repositoryOwner, - repositoryName, - }: { - repositoryOwner: string; - repositoryName: string; - }): Promise { - const maybeRepo = await this.prisma.repository.findUnique({ - where: { - owner_name: { - owner: repositoryOwner, - name: repositoryName, - }, - }, - }); - return convertPrismaRepo(maybeRepo); + if (repositoryName != null) { + where.repositoryName = repositoryName; } - - async deleteRepository({ - repositoryOwner, - repositoryName, - }: { - repositoryOwner: string; - repositoryName: string; - }): Promise { - await this.prisma.repository.delete({ - where: { - owner_name: { - owner: repositoryOwner, - name: repositoryName, - }, - }, - }); + if (organizationId != null) { + where.repositoryOwnerOrganizationId = organizationId; } + const repos = await this.prisma.repository.findMany({ + skip: page * pageSize, + take: pageSize, + where, + orderBy: { + fullName: "asc", + }, + }); + + return { + repositories: repos + .map(convertPrismaRepo) + .filter((g): g is FernRepository => g != null), + }; + } + + async getRepository({ + repositoryOwner, + repositoryName, + }: { + repositoryOwner: string; + repositoryName: string; + }): Promise { + const maybeRepo = await this.prisma.repository.findUnique({ + where: { + owner_name: { + owner: repositoryOwner, + name: repositoryName, + }, + }, + }); + return convertPrismaRepo(maybeRepo); + } + + async deleteRepository({ + repositoryOwner, + repositoryName, + }: { + repositoryOwner: string; + repositoryName: string; + }): Promise { + await this.prisma.repository.delete({ + where: { + owner_name: { + owner: repositoryOwner, + name: repositoryName, + }, + }, + }); + } } -function convertPrismaRepo(maybeRepo: prisma.Repository | null): FernRepository | undefined { - if (!maybeRepo) { - return undefined; - } +function convertPrismaRepo( + maybeRepo: prisma.Repository | null +): FernRepository | undefined { + if (!maybeRepo) { + return undefined; + } - return readBuffer(maybeRepo.rawRepository) as FernRepository; + return readBuffer(maybeRepo.rawRepository) as FernRepository; } -function convertPrismaPullRequest(maybePR: prisma.PullRequest | null): PullRequest | undefined { - if (!maybePR) { - return undefined; - } +function convertPrismaPullRequest( + maybePR: prisma.PullRequest | null +): PullRequest | undefined { + if (!maybePR) { + return undefined; + } - return { - pullRequestNumber: maybePR.pullRequestNumber, - repositoryName: maybePR.repositoryName, - repositoryOwner: maybePR.repositoryOwner, - author: maybePR.author != null ? (readBuffer(maybePR.author) as GithubUser) : undefined, - reviewers: readBuffer(maybePR.reviewers) as PullRequestReviewer[], - checks: readBuffer(maybePR.checks) as CheckRun[], - title: maybePR.title, - url: FdrAPI.Url(maybePR.url), - state: maybePR.state, - createdAt: maybePR.createdAt.toISOString(), - updatedAt: maybePR.updatedAt?.toISOString(), - closedAt: maybePR.closedAt?.toISOString(), - mergedAt: maybePR.mergedAt?.toISOString(), - }; + return { + pullRequestNumber: maybePR.pullRequestNumber, + repositoryName: maybePR.repositoryName, + repositoryOwner: maybePR.repositoryOwner, + author: + maybePR.author != null + ? (readBuffer(maybePR.author) as GithubUser) + : undefined, + reviewers: readBuffer(maybePR.reviewers) as PullRequestReviewer[], + checks: readBuffer(maybePR.checks) as CheckRun[], + title: maybePR.title, + url: FdrAPI.Url(maybePR.url), + state: maybePR.state, + createdAt: maybePR.createdAt.toISOString(), + updatedAt: maybePR.updatedAt?.toISOString(), + closedAt: maybePR.closedAt?.toISOString(), + mergedAt: maybePR.mergedAt?.toISOString(), + }; } diff --git a/servers/fdr/src/db/registrations/DocsRegistrationDao.ts b/servers/fdr/src/db/registrations/DocsRegistrationDao.ts index 05d7fa193e..f5eec39113 100644 --- a/servers/fdr/src/db/registrations/DocsRegistrationDao.ts +++ b/servers/fdr/src/db/registrations/DocsRegistrationDao.ts @@ -5,47 +5,52 @@ import { readBuffer, writeBuffer } from "../../util"; import { ParsedBaseUrl } from "../../util/ParsedBaseUrl"; export interface DocsRegistrationInfo { - fernUrl: ParsedBaseUrl; - customUrls: ParsedBaseUrl[]; - orgId: FdrAPI.OrgId; - s3FileInfos: Record; - isPreview: boolean; - authType: AuthType; + fernUrl: ParsedBaseUrl; + customUrls: ParsedBaseUrl[]; + orgId: FdrAPI.OrgId; + s3FileInfos: Record; + isPreview: boolean; + authType: AuthType; } export class DocsRegistrationDao { - constructor(private readonly prisma: PrismaClient) {} + constructor(private readonly prisma: PrismaClient) {} - public async storeDocsRegistrationById( - id: DocsV1Write.DocsRegistrationId, - info: DocsRegistrationInfo, - ): Promise { - await this.prisma.docsRegistrations.create({ - data: { - fernURL: info.fernUrl.getFullUrl(), - registrationID: id, - authType: info.authType, - customURLs: info.customUrls.map((parsedURL) => parsedURL.getFullUrl()), - isPreview: info.isPreview, - orgID: info.orgId, - s3FileInfos: writeBuffer(info.s3FileInfos), - }, - }); - } + public async storeDocsRegistrationById( + id: DocsV1Write.DocsRegistrationId, + info: DocsRegistrationInfo + ): Promise { + await this.prisma.docsRegistrations.create({ + data: { + fernURL: info.fernUrl.getFullUrl(), + registrationID: id, + authType: info.authType, + customURLs: info.customUrls.map((parsedURL) => parsedURL.getFullUrl()), + isPreview: info.isPreview, + orgID: info.orgId, + s3FileInfos: writeBuffer(info.s3FileInfos), + }, + }); + } - public async getDocsRegistrationById(id: DocsV1Write.DocsRegistrationId): Promise { - const response = await this.prisma.docsRegistrations.findFirstOrThrow({ - where: { - registrationID: id, - }, - }); - return { - authType: response.authType, - customUrls: response.customURLs.map((url) => ParsedBaseUrl.parse(url)), - isPreview: response.isPreview, - orgId: FdrAPI.OrgId(response.orgID), - fernUrl: ParsedBaseUrl.parse(response.fernURL), - s3FileInfos: readBuffer(response.s3FileInfos) as any as Record, - }; - } + public async getDocsRegistrationById( + id: DocsV1Write.DocsRegistrationId + ): Promise { + const response = await this.prisma.docsRegistrations.findFirstOrThrow({ + where: { + registrationID: id, + }, + }); + return { + authType: response.authType, + customUrls: response.customURLs.map((url) => ParsedBaseUrl.parse(url)), + isPreview: response.isPreview, + orgId: FdrAPI.OrgId(response.orgID), + fernUrl: ParsedBaseUrl.parse(response.fernURL), + s3FileInfos: readBuffer(response.s3FileInfos) as any as Record< + DocsV1Write.FilePath, + S3DocsFileInfo + >, + }; + } } diff --git a/servers/fdr/src/db/sdk/SdkDao.ts b/servers/fdr/src/db/sdk/SdkDao.ts index d69301fdbb..6ea091e897 100644 --- a/servers/fdr/src/db/sdk/SdkDao.ts +++ b/servers/fdr/src/db/sdk/SdkDao.ts @@ -6,198 +6,226 @@ import { SdkIdFactory } from "../snippets/SdkIdFactory"; import { SdkId } from "../types"; type PrismaTransaction = Omit< - PrismaClient, - "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" + PrismaClient, + "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" >; export interface SdkIdForPackage { - typescriptSdk?: APIV1Write.TypescriptPackage & { sdkId: string }; - pythonSdk?: APIV1Write.PythonPackage & { sdkId: string }; - goSdk?: APIV1Write.GoModule & { sdkId: string }; - rubySdk?: APIV1Write.RubyGem & { sdkId: string }; - javaSdk?: APIV1Write.JavaCoordinate & { sdkId: string }; + typescriptSdk?: APIV1Write.TypescriptPackage & { sdkId: string }; + pythonSdk?: APIV1Write.PythonPackage & { sdkId: string }; + goSdk?: APIV1Write.GoModule & { sdkId: string }; + rubySdk?: APIV1Write.RubyGem & { sdkId: string }; + javaSdk?: APIV1Write.JavaCoordinate & { sdkId: string }; } interface SdkPackageRequest { - sdkPackage: string; - language: Language; - version?: string; + sdkPackage: string; + language: Language; + version?: string; } export interface SdkPackage extends SdkPackageRequest { - id: string; - sdk: Buffer; + id: string; + sdk: Buffer; } export interface SdkDao { - createSdk(sdk: SdkPackage, tx?: PrismaClient): Promise; - - createSdkIfNotExists(sdk: SdkPackage, tx?: PrismaClient, overwrite?: boolean): Promise; - - getSdkIdsForPackages(snippetConfig: APIV1Write.SnippetsConfig): Promise; - - getSdkIdForPackage({ sdkPackage, language, version }: SdkPackageRequest): Promise; + createSdk(sdk: SdkPackage, tx?: PrismaClient): Promise; + + createSdkIfNotExists( + sdk: SdkPackage, + tx?: PrismaClient, + overwrite?: boolean + ): Promise; + + getSdkIdsForPackages( + snippetConfig: APIV1Write.SnippetsConfig + ): Promise; + + getSdkIdForPackage({ + sdkPackage, + language, + version, + }: SdkPackageRequest): Promise; } export class SdkDaoImpl implements SdkDao { - constructor(private readonly prisma: PrismaClient) {} - - public async createManySdks(sdks: SdkPackage[], tx?: PrismaTransaction): Promise { - await (tx ?? this.prisma).sdk.createMany({ - data: sdks.map((sdk) => ({ - id: sdk.id, - package: sdk.sdkPackage, - language: sdk.language, - version: sdk.version, - sdk: sdk.sdk, - })), - skipDuplicates: true, + constructor(private readonly prisma: PrismaClient) {} + + public async createManySdks( + sdks: SdkPackage[], + tx?: PrismaTransaction + ): Promise { + await (tx ?? this.prisma).sdk.createMany({ + data: sdks.map((sdk) => ({ + id: sdk.id, + package: sdk.sdkPackage, + language: sdk.language, + version: sdk.version, + sdk: sdk.sdk, + })), + skipDuplicates: true, + }); + } + + public async createSdk( + sdk: SdkPackage, + tx?: PrismaTransaction + ): Promise { + await (tx ?? this.prisma).sdk.create({ + data: { + id: sdk.id, + package: sdk.sdkPackage, + language: sdk.language, + version: sdk.version, + sdk: sdk.sdk, + }, + }); + } + + public async createSdkIfNotExists( + sdk: SdkPackage, + tx?: PrismaTransaction, + overwrite?: boolean + ): Promise { + const dbSdk = await (tx ?? this.prisma).sdk.findUnique({ + where: { + id: sdk.id, + }, + }); + if (dbSdk == null || overwrite === true) { + // Overwrite the SDK and all of its snippets that already exist. + if (dbSdk !== null && overwrite === true) { + await (tx ?? this.prisma).sdk.delete({ + where: { + id: sdk.id, + }, }); + } + await this.createSdk(sdk, tx); } - - public async createSdk(sdk: SdkPackage, tx?: PrismaTransaction): Promise { - await (tx ?? this.prisma).sdk.create({ - data: { - id: sdk.id, - package: sdk.sdkPackage, - language: sdk.language, - version: sdk.version, - sdk: sdk.sdk, - }, - }); + } + + public async getSdkIdsForPackages( + snippetConfig: APIV1Write.SnippetsConfig + ): Promise { + const result: SdkIdForPackage = {}; + if (snippetConfig.typescriptSdk != null) { + const sdkId = await this.getSdkIdForPackage({ + sdkPackage: snippetConfig.typescriptSdk.package, + language: Language.TYPESCRIPT, + version: snippetConfig.typescriptSdk.version, + }); + if (sdkId != null) { + result.typescriptSdk = { ...snippetConfig.typescriptSdk, sdkId }; + } } - - public async createSdkIfNotExists(sdk: SdkPackage, tx?: PrismaTransaction, overwrite?: boolean): Promise { - const dbSdk = await (tx ?? this.prisma).sdk.findUnique({ - where: { - id: sdk.id, - }, - }); - if (dbSdk == null || overwrite === true) { - // Overwrite the SDK and all of its snippets that already exist. - if (dbSdk !== null && overwrite === true) { - await (tx ?? this.prisma).sdk.delete({ - where: { - id: sdk.id, - }, - }); - } - await this.createSdk(sdk, tx); - } + if (snippetConfig.pythonSdk != null) { + const sdkId = await this.getSdkIdForPackage({ + sdkPackage: snippetConfig.pythonSdk.package, + language: Language.PYTHON, + version: snippetConfig.pythonSdk.version, + }); + if (sdkId != null) { + result.pythonSdk = { ...snippetConfig.pythonSdk, sdkId }; + } } - - public async getSdkIdsForPackages(snippetConfig: APIV1Write.SnippetsConfig): Promise { - const result: SdkIdForPackage = {}; - if (snippetConfig.typescriptSdk != null) { - const sdkId = await this.getSdkIdForPackage({ - sdkPackage: snippetConfig.typescriptSdk.package, - language: Language.TYPESCRIPT, - version: snippetConfig.typescriptSdk.version, - }); - if (sdkId != null) { - result.typescriptSdk = { ...snippetConfig.typescriptSdk, sdkId }; - } - } - if (snippetConfig.pythonSdk != null) { - const sdkId = await this.getSdkIdForPackage({ - sdkPackage: snippetConfig.pythonSdk.package, - language: Language.PYTHON, - version: snippetConfig.pythonSdk.version, - }); - if (sdkId != null) { - result.pythonSdk = { ...snippetConfig.pythonSdk, sdkId }; - } - } - if (snippetConfig.javaSdk != null) { - const sdkId = await this.getSdkIdForPackage({ - sdkPackage: snippetConfig.javaSdk.coordinate, - language: Language.JAVA, - version: snippetConfig.javaSdk.version, - }); - if (sdkId != null) { - result.javaSdk = { ...snippetConfig.javaSdk, sdkId }; - } - } - if (snippetConfig.goSdk != null) { - const sdkId = await this.getSdkIdForPackage({ - sdkPackage: snippetConfig.goSdk.githubRepo, - language: Language.GO, - version: snippetConfig.goSdk.version, - }); - if (sdkId != null) { - result.goSdk = { ...snippetConfig.goSdk, sdkId }; - } - } - if (snippetConfig.rubySdk != null) { - const sdkId = await this.getSdkIdForPackage({ - sdkPackage: snippetConfig.rubySdk.gem, - language: Language.RUBY, - version: snippetConfig.rubySdk.version, - }); - if (sdkId != null) { - result.rubySdk = { ...snippetConfig.rubySdk, sdkId }; - } - } - - return result; + if (snippetConfig.javaSdk != null) { + const sdkId = await this.getSdkIdForPackage({ + sdkPackage: snippetConfig.javaSdk.coordinate, + language: Language.JAVA, + version: snippetConfig.javaSdk.version, + }); + if (sdkId != null) { + result.javaSdk = { ...snippetConfig.javaSdk, sdkId }; + } + } + if (snippetConfig.goSdk != null) { + const sdkId = await this.getSdkIdForPackage({ + sdkPackage: snippetConfig.goSdk.githubRepo, + language: Language.GO, + version: snippetConfig.goSdk.version, + }); + if (sdkId != null) { + result.goSdk = { ...snippetConfig.goSdk, sdkId }; + } + } + if (snippetConfig.rubySdk != null) { + const sdkId = await this.getSdkIdForPackage({ + sdkPackage: snippetConfig.rubySdk.gem, + language: Language.RUBY, + version: snippetConfig.rubySdk.version, + }); + if (sdkId != null) { + result.rubySdk = { ...snippetConfig.rubySdk, sdkId }; + } } - public async getSdkIdForPackage({ sdkPackage, language, version }: SdkPackageRequest): Promise { - let id: string | undefined; - if (version != null) { - switch (language) { - case Language.TYPESCRIPT: - id = SdkIdFactory.fromTypescript({ package: sdkPackage, version }); - break; - case Language.PYTHON: - id = SdkIdFactory.fromPython({ package: sdkPackage, version }); - break; - case Language.GO: - id = SdkIdFactory.fromGo({ githubRepo: sdkPackage, version }); - break; - case Language.RUBY: - id = SdkIdFactory.fromRuby({ gem: sdkPackage, version }); - break; - default: - break; - } - if (id != null) { - return id; - } - } - - // Get all SDK rows ordered by creation date - const sdkRows = await this.prisma.sdk.findMany({ - select: { - id: true, - }, - where: { - package: sdkPackage, - language, - }, - orderBy: { - createdAt: "desc", - }, - take: 10, - }); - - // Find first SDK that has snippets - for (const sdkRow of sdkRows) { - const hasSnippets = await this.prisma.snippet.findFirst({ - where: { - sdkId: sdkRow.id, - }, - }); - - if (hasSnippets) { - return sdkRow?.id; - } - } - - // If no SDKs have snippets, return the most recent one - const sdkRow = sdkRows[0]; + return result; + } + + public async getSdkIdForPackage({ + sdkPackage, + language, + version, + }: SdkPackageRequest): Promise { + let id: string | undefined; + if (version != null) { + switch (language) { + case Language.TYPESCRIPT: + id = SdkIdFactory.fromTypescript({ package: sdkPackage, version }); + break; + case Language.PYTHON: + id = SdkIdFactory.fromPython({ package: sdkPackage, version }); + break; + case Language.GO: + id = SdkIdFactory.fromGo({ githubRepo: sdkPackage, version }); + break; + case Language.RUBY: + id = SdkIdFactory.fromRuby({ gem: sdkPackage, version }); + break; + default: + break; + } + if (id != null) { + return id; + } + } - LOGGER.info(`Looking for latest registered SDK ${sdkPackage} and found id ${sdkRow?.id}`); + // Get all SDK rows ordered by creation date + const sdkRows = await this.prisma.sdk.findMany({ + select: { + id: true, + }, + where: { + package: sdkPackage, + language, + }, + orderBy: { + createdAt: "desc", + }, + take: 10, + }); + + // Find first SDK that has snippets + for (const sdkRow of sdkRows) { + const hasSnippets = await this.prisma.snippet.findFirst({ + where: { + sdkId: sdkRow.id, + }, + }); + + if (hasSnippets) { return sdkRow?.id; + } } + + // If no SDKs have snippets, return the most recent one + const sdkRow = sdkRows[0]; + + LOGGER.info( + `Looking for latest registered SDK ${sdkPackage} and found id ${sdkRow?.id}` + ); + return sdkRow?.id; + } } diff --git a/servers/fdr/src/db/snippetApis/SnippetAPIsDao.ts b/servers/fdr/src/db/snippetApis/SnippetAPIsDao.ts index 10f6991d5a..f91ef03287 100644 --- a/servers/fdr/src/db/snippetApis/SnippetAPIsDao.ts +++ b/servers/fdr/src/db/snippetApis/SnippetAPIsDao.ts @@ -2,70 +2,82 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { PrismaClient, SnippetApi } from "@prisma/client"; export interface LoadSnippetAPIRequest { - orgId: FdrAPI.OrgId; - apiName: string; + orgId: FdrAPI.OrgId; + apiName: string; } export interface LoadSnippetAPIsRequest { - orgIds: FdrAPI.OrgId[]; - apiName: string | undefined; + orgIds: FdrAPI.OrgId[]; + apiName: string | undefined; } export interface SnippetAPIsDao { - createSnippetAPI({ apiName, orgId }: { apiName: string; orgId: FdrAPI.OrgId }): Promise; + createSnippetAPI({ + apiName, + orgId, + }: { + apiName: string; + orgId: FdrAPI.OrgId; + }): Promise; - loadSnippetAPI({ - loadSnippetAPIRequest, - }: { - loadSnippetAPIRequest: LoadSnippetAPIRequest; - }): Promise; + loadSnippetAPI({ + loadSnippetAPIRequest, + }: { + loadSnippetAPIRequest: LoadSnippetAPIRequest; + }): Promise; - loadSnippetAPIs({ - loadSnippetAPIsRequest, - }: { - loadSnippetAPIsRequest: LoadSnippetAPIsRequest; - }): Promise; + loadSnippetAPIs({ + loadSnippetAPIsRequest, + }: { + loadSnippetAPIsRequest: LoadSnippetAPIsRequest; + }): Promise; } export class SnippetAPIsDaoImpl implements SnippetAPIsDao { - constructor(private readonly prisma: PrismaClient) {} + constructor(private readonly prisma: PrismaClient) {} - public async createSnippetAPI({ apiName, orgId }: { apiName: string; orgId: FdrAPI.OrgId }): Promise { - await this.prisma.snippetApi.create({ - data: { - apiName, - orgId, - }, - }); - } + public async createSnippetAPI({ + apiName, + orgId, + }: { + apiName: string; + orgId: FdrAPI.OrgId; + }): Promise { + await this.prisma.snippetApi.create({ + data: { + apiName, + orgId, + }, + }); + } - public async loadSnippetAPI({ - loadSnippetAPIRequest, - }: { - loadSnippetAPIRequest: LoadSnippetAPIRequest; - }): Promise { - return await this.prisma.snippetApi.findUnique({ - where: { - orgId_apiName: { - orgId: loadSnippetAPIRequest.orgId, - apiName: loadSnippetAPIRequest.apiName, - }, - }, - }); - } + public async loadSnippetAPI({ + loadSnippetAPIRequest, + }: { + loadSnippetAPIRequest: LoadSnippetAPIRequest; + }): Promise { + return await this.prisma.snippetApi.findUnique({ + where: { + orgId_apiName: { + orgId: loadSnippetAPIRequest.orgId, + apiName: loadSnippetAPIRequest.apiName, + }, + }, + }); + } - public async loadSnippetAPIs({ - loadSnippetAPIsRequest, - }: { - loadSnippetAPIsRequest: LoadSnippetAPIsRequest; - }): Promise { - return await this.prisma.snippetApi.findMany({ - where: { - orgId: { - in: loadSnippetAPIsRequest.orgIds, - }, - apiName: loadSnippetAPIsRequest.apiName, - }, - }); - } + public async loadSnippetAPIs({ + loadSnippetAPIsRequest, + }: { + loadSnippetAPIsRequest: LoadSnippetAPIsRequest; + }): Promise { + return await this.prisma.snippetApi.findMany({ + where: { + orgId: { + in: loadSnippetAPIsRequest.orgIds, + }, + apiName: loadSnippetAPIsRequest.apiName, + }, + }); + } } diff --git a/servers/fdr/src/db/snippets/EndpointSnippetCollectors.ts b/servers/fdr/src/db/snippets/EndpointSnippetCollectors.ts index 06dd1afa10..f0297602f2 100644 --- a/servers/fdr/src/db/snippets/EndpointSnippetCollectors.ts +++ b/servers/fdr/src/db/snippets/EndpointSnippetCollectors.ts @@ -1,43 +1,49 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; export class EndpointSnippetCollector { - private snippetsByEndpoint: Record = {}; - private snippetsByEndpointId: Record = {}; + private snippetsByEndpoint: Record< + FdrAPI.EndpointPathLiteral, + FdrAPI.SnippetsByEndpointMethod + > = {}; + private snippetsByEndpointId: Record = {}; - public collect({ - endpointPath, - endpointMethod, - snippet, - identifierOverride, - }: { - endpointPath: FdrAPI.EndpointPathLiteral; - endpointMethod: "POST" | "PUT" | "GET" | "PATCH" | "DELETE"; - snippet: FdrAPI.Snippet; - identifierOverride: string | undefined; - }) { - if (identifierOverride != null) { - if (this.snippetsByEndpointId[identifierOverride] == null) { - this.snippetsByEndpointId[identifierOverride] = []; - } - this.snippetsByEndpointId[identifierOverride]?.push(snippet); - } - if (this.snippetsByEndpoint[endpointPath] == null) { - this.snippetsByEndpoint[endpointPath] = { - PUT: [], - POST: [], - GET: [], - PATCH: [], - DELETE: [], - }; - } - this.snippetsByEndpoint[endpointPath]?.[endpointMethod]?.push(snippet); + public collect({ + endpointPath, + endpointMethod, + snippet, + identifierOverride, + }: { + endpointPath: FdrAPI.EndpointPathLiteral; + endpointMethod: "POST" | "PUT" | "GET" | "PATCH" | "DELETE"; + snippet: FdrAPI.Snippet; + identifierOverride: string | undefined; + }) { + if (identifierOverride != null) { + if (this.snippetsByEndpointId[identifierOverride] == null) { + this.snippetsByEndpointId[identifierOverride] = []; + } + this.snippetsByEndpointId[identifierOverride]?.push(snippet); } - - public get(): Record { - return this.snippetsByEndpoint; + if (this.snippetsByEndpoint[endpointPath] == null) { + this.snippetsByEndpoint[endpointPath] = { + PUT: [], + POST: [], + GET: [], + PATCH: [], + DELETE: [], + }; } + this.snippetsByEndpoint[endpointPath]?.[endpointMethod]?.push(snippet); + } - public getByIdentifierOverride(): Record { - return this.snippetsByEndpointId; - } + public get(): Record< + FdrAPI.EndpointPathLiteral, + FdrAPI.SnippetsByEndpointMethod + > { + return this.snippetsByEndpoint; + } + + public getByIdentifierOverride(): Record { + return this.snippetsByEndpointId; + } } diff --git a/servers/fdr/src/db/snippets/SdkIdFactory.ts b/servers/fdr/src/db/snippets/SdkIdFactory.ts index 6b567c56ac..b215e3a0f6 100644 --- a/servers/fdr/src/db/snippets/SdkIdFactory.ts +++ b/servers/fdr/src/db/snippets/SdkIdFactory.ts @@ -1,23 +1,23 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; export class SdkIdFactory { - public static fromTypescript(sdk: FdrAPI.TypeScriptSdk): string { - return `typescript|${sdk.package}|${sdk.version}`; - } + public static fromTypescript(sdk: FdrAPI.TypeScriptSdk): string { + return `typescript|${sdk.package}|${sdk.version}`; + } - public static fromPython(sdk: FdrAPI.PythonSdk): string { - return `python|${sdk.package}|${sdk.version}`; - } + public static fromPython(sdk: FdrAPI.PythonSdk): string { + return `python|${sdk.package}|${sdk.version}`; + } - public static fromGo(sdk: FdrAPI.GoSdk): string { - return `go|${sdk.githubRepo}|${sdk.version}`; - } + public static fromGo(sdk: FdrAPI.GoSdk): string { + return `go|${sdk.githubRepo}|${sdk.version}`; + } - public static fromRuby(sdk: FdrAPI.RubySdk): string { - return `ruby|${sdk.gem}|${sdk.version}`; - } + public static fromRuby(sdk: FdrAPI.RubySdk): string { + return `ruby|${sdk.gem}|${sdk.version}`; + } - public static fromJava(sdk: FdrAPI.JavaSdk): string { - return `java|${sdk.group}|${sdk.artifact}|${sdk.version}`; - } + public static fromJava(sdk: FdrAPI.JavaSdk): string { + return `java|${sdk.group}|${sdk.artifact}|${sdk.version}`; + } } diff --git a/servers/fdr/src/db/snippets/SnippetTemplate.ts b/servers/fdr/src/db/snippets/SnippetTemplate.ts index c30f2c3a17..78bb0fc9ec 100644 --- a/servers/fdr/src/db/snippets/SnippetTemplate.ts +++ b/servers/fdr/src/db/snippets/SnippetTemplate.ts @@ -6,361 +6,392 @@ import { WithoutQuestionMarks, readBuffer, writeBuffer } from "../../util"; import { SdkDaoImpl, SdkPackage } from "../sdk/SdkDao"; import { SdkIdFactory } from "./SdkIdFactory"; import { - getLanguageFromRequest, - getPackageNameFromSdkRequest, - getSdkFromSdkRequest, + getLanguageFromRequest, + getPackageNameFromSdkRequest, + getSdkFromSdkRequest, } from "./getPackageNameFromSdkSnippetsCreate"; export interface LoadSnippetAPIRequest { - orgId: string; - apiName: string; + orgId: string; + apiName: string; } export interface LoadSnippetAPIsRequest { - orgIds: string[]; - apiName: string | undefined; + orgIds: string[]; + apiName: string | undefined; } export type SnippetTemplatesByEndpoint = Record< - FdrAPI.EndpointPathLiteral, - Record + FdrAPI.EndpointPathLiteral, + Record >; -export type SnippetTemplatesByEndpointIdentifier = Record; +export type SnippetTemplatesByEndpointIdentifier = Record< + string, + APIV1Read.EndpointSnippetTemplates +>; export interface SnippetTemplateDao { - loadSnippetTemplate({ - loadSnippetTemplateRequest, - }: { - loadSnippetTemplateRequest: GetSnippetTemplate; - }): Promise; + loadSnippetTemplate({ + loadSnippetTemplateRequest, + }: { + loadSnippetTemplateRequest: GetSnippetTemplate; + }): Promise; - loadSnippetTemplatesByEndpoint(opts: { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - sdkRequests: FdrAPI.SdkRequest[]; - definition: APIV1Write.ApiDefinition; - }): Promise; + loadSnippetTemplatesByEndpoint(opts: { + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + sdkRequests: FdrAPI.SdkRequest[]; + definition: APIV1Write.ApiDefinition; + }): Promise; - loadSnippetTemplatesByEndpointIdentifier(opts: { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - sdkRequests: FdrAPI.SdkRequest[]; - definition: APIV1Write.ApiDefinition; - }): Promise; + loadSnippetTemplatesByEndpointIdentifier(opts: { + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + sdkRequests: FdrAPI.SdkRequest[]; + definition: APIV1Write.ApiDefinition; + }): Promise; - storeSnippetTemplate({ - storeSnippetsInfo, - }: { - storeSnippetsInfo: FdrAPI.RegisterSnippetTemplateBatchRequest; - }): Promise; + storeSnippetTemplate({ + storeSnippetsInfo, + }: { + storeSnippetsInfo: FdrAPI.RegisterSnippetTemplateBatchRequest; + }): Promise; } export class SnippetTemplateDaoImpl implements SnippetTemplateDao { - constructor(private readonly prisma: PrismaClient) {} - async loadSnippetTemplatesByEndpointIdentifier({ - orgId, - apiId, - sdkRequests, - definition, - }: { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - sdkRequests: FdrAPI.SdkRequest[]; - definition: APIV1Write.ApiDefinition; - }): Promise { - const endpoints: APIV1Write.EndpointDefinition[] = []; - for (const endpoint of definition.rootPackage.endpoints) { - endpoints.push(endpoint); - } - - for (const subpackage of Object.values(definition.subpackages)) { - for (const endpoint of subpackage.endpoints) { - endpoints.push(endpoint); - } - } - - const toRet: Record = {}; - for (const endpoint of endpoints) { - for (const sdk of sdkRequests) { - if (sdk.type !== "typescript" && sdk.type !== "python") { - continue; - } - const result = await this.loadSnippetTemplate({ - loadSnippetTemplateRequest: { - sdk, - orgId, - apiId, - endpointId: { - path: getEndpointPathAsString(endpoint), - method: endpoint.method, - identifierOverride: undefined, - }, - }, - }); - if (result?.endpointId.identifierOverride != null) { - const template = { - [sdk.type]: result.snippetTemplate, - ...(toRet[result.endpointId.identifierOverride] ?? {}), - } as APIV1Read.EndpointSnippetTemplates; - toRet[result.endpointId.identifierOverride] = template; - } - } - } - - return toRet; + constructor(private readonly prisma: PrismaClient) {} + async loadSnippetTemplatesByEndpointIdentifier({ + orgId, + apiId, + sdkRequests, + definition, + }: { + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + sdkRequests: FdrAPI.SdkRequest[]; + definition: APIV1Write.ApiDefinition; + }): Promise { + const endpoints: APIV1Write.EndpointDefinition[] = []; + for (const endpoint of definition.rootPackage.endpoints) { + endpoints.push(endpoint); } - async getSdkFromSdkRequest(request: FdrAPI.SdkRequest): Promise { - if (request.version != null) { - return { ...request, version: request.version }; - } else { - const packageName = getPackageNameFromSdkRequest(request); - const language = getLanguageFromRequest({ sdk: request }); - const sdkDao = await this.prisma.sdk.findFirst({ - select: { - version: true, - }, - where: { - // package: packageName, - language, - }, - orderBy: { - createdAt: "desc", - }, - }); - - if (sdkDao == null) { - throw new BadRequestError(`No SDK found for the given request: ${language} ${packageName}`); - } else if (sdkDao.version == null) { - throw new BadRequestError("No version for SDK found for the given request"); - } - - return { - ...request, - version: sdkDao.version, - }; - } + for (const subpackage of Object.values(definition.subpackages)) { + for (const endpoint of subpackage.endpoints) { + endpoints.push(endpoint); + } } - public async loadSnippetTemplate({ - loadSnippetTemplateRequest, - }: { - loadSnippetTemplateRequest: GetSnippetTemplate; - }): Promise { - const sdkFromRequest = await getSdkFromSdkRequest(this.prisma, loadSnippetTemplateRequest.sdk); - - let snippetTemplate = null; - if (loadSnippetTemplateRequest.endpointId.identifierOverride != null) { - snippetTemplate = await this.prisma.snippetTemplate.findFirst({ - where: { - orgId: loadSnippetTemplateRequest.orgId, - apiName: loadSnippetTemplateRequest.apiId, - identifierOverride: loadSnippetTemplateRequest.endpointId.identifierOverride, - sdkId: this.getSdkId(sdkFromRequest), - }, - }); - } - if (snippetTemplate == null) { - snippetTemplate = await this.prisma.snippetTemplate.findFirst({ - where: { - orgId: loadSnippetTemplateRequest.orgId, - apiName: loadSnippetTemplateRequest.apiId, - endpointPath: loadSnippetTemplateRequest.endpointId?.path, - endpointMethod: loadSnippetTemplateRequest.endpointId?.method, - sdkId: this.getSdkId(sdkFromRequest), - }, - }); - } - - if (!snippetTemplate) { - return null; + const toRet: Record = {}; + for (const endpoint of endpoints) { + for (const sdk of sdkRequests) { + if (sdk.type !== "typescript" && sdk.type !== "python") { + continue; } - return { - apiDefinitionId: FdrAPI.ApiDefinitionId(snippetTemplate.apiDefinitionId), - additionalTemplates: undefined, + const result = await this.loadSnippetTemplate({ + loadSnippetTemplateRequest: { + sdk, + orgId, + apiId, endpointId: { - path: FdrAPI.EndpointPathLiteral(snippetTemplate.endpointPath), - method: snippetTemplate.endpointMethod, - identifierOverride: snippetTemplate.identifierOverride ?? undefined, - }, - sdk: sdkFromRequest, - snippetTemplate: { - type: snippetTemplate.version, - functionInvocation: readBuffer(snippetTemplate.functionInvocation) as FdrAPI.Template, - clientInstantiation: readBuffer(snippetTemplate.clientInstantiation) as string, - }, - }; - } - - private getSdkId(sdk: FdrAPI.Sdk): string { - switch (sdk.type) { - case "typescript": - return SdkIdFactory.fromTypescript(sdk); - case "python": - return SdkIdFactory.fromPython(sdk); - case "go": - return SdkIdFactory.fromGo(sdk); - case "ruby": - return SdkIdFactory.fromRuby(sdk); - case "java": - return SdkIdFactory.fromJava(sdk); - } - } - - public async storeSnippetTemplate({ - storeSnippetsInfo, - }: { - storeSnippetsInfo: FdrAPI.RegisterSnippetTemplateBatchRequest; - }): Promise { - const dbApi = await this.prisma.snippetApi.findUnique({ - where: { - orgId_apiName: { - orgId: storeSnippetsInfo.orgId, - apiName: storeSnippetsInfo.apiId, - }, + path: getEndpointPathAsString(endpoint), + method: endpoint.method, + identifierOverride: undefined, }, + }, }); - if (dbApi == null) { - await this.prisma.snippetApi.create({ - data: { - orgId: storeSnippetsInfo.orgId, - apiName: storeSnippetsInfo.apiId, - }, - }); + if (result?.endpointId.identifierOverride != null) { + const template = { + [sdk.type]: result.snippetTemplate, + ...(toRet[result.endpointId.identifierOverride] ?? {}), + } as APIV1Read.EndpointSnippetTemplates; + toRet[result.endpointId.identifierOverride] = template; } - const sdkDao = new SdkDaoImpl(this.prisma); + } + } - await this.prisma.$transaction(async (tx) => { - const snippets: Prisma.Enumerable> = []; - const sdks: Prisma.Enumerable = []; - await Promise.all( - storeSnippetsInfo.snippets.map(async (snippet) => { - const sdkId = this.getSdkId(snippet.sdk); + return toRet; + } - snippets.push({ - id: uuidv4(), - orgId: storeSnippetsInfo.orgId, - apiName: storeSnippetsInfo.apiId, - apiDefinitionId: storeSnippetsInfo.apiDefinitionId, - endpointPath: snippet.endpointId.path, - endpointMethod: snippet.endpointId.method, - identifierOverride: snippet.endpointId.identifierOverride, - sdkId, - version: snippet.snippetTemplate.type, - functionInvocation: writeBuffer(snippet.snippetTemplate.functionInvocation), - clientInstantiation: writeBuffer(snippet.snippetTemplate.clientInstantiation), - }); + async getSdkFromSdkRequest(request: FdrAPI.SdkRequest): Promise { + if (request.version != null) { + return { ...request, version: request.version }; + } else { + const packageName = getPackageNameFromSdkRequest(request); + const language = getLanguageFromRequest({ sdk: request }); + const sdkDao = await this.prisma.sdk.findFirst({ + select: { + version: true, + }, + where: { + // package: packageName, + language, + }, + orderBy: { + createdAt: "desc", + }, + }); - sdks.push({ - id: sdkId, - sdkPackage: getPackageNameFromSdkRequest(snippet.sdk), - version: snippet.sdk.version, - language: getLanguageFromRequest({ sdk: snippet.sdk }), - sdk: writeBuffer(snippet.sdk), - }); - }), - ); + if (sdkDao == null) { + throw new BadRequestError( + `No SDK found for the given request: ${language} ${packageName}` + ); + } else if (sdkDao.version == null) { + throw new BadRequestError( + "No version for SDK found for the given request" + ); + } - await sdkDao.createManySdks(sdks, tx); - await tx.snippetTemplate.createMany({ - data: snippets, - }); - }); + return { + ...request, + version: sdkDao.version, + }; } + } - public DEFAULT_ENDPOINT_SNIPPET_TEMPLATES: Record = { - PATCH: { - typescript: undefined, - python: undefined, - }, - POST: { - typescript: undefined, - python: undefined, + public async loadSnippetTemplate({ + loadSnippetTemplateRequest, + }: { + loadSnippetTemplateRequest: GetSnippetTemplate; + }): Promise { + const sdkFromRequest = await getSdkFromSdkRequest( + this.prisma, + loadSnippetTemplateRequest.sdk + ); + + let snippetTemplate = null; + if (loadSnippetTemplateRequest.endpointId.identifierOverride != null) { + snippetTemplate = await this.prisma.snippetTemplate.findFirst({ + where: { + orgId: loadSnippetTemplateRequest.orgId, + apiName: loadSnippetTemplateRequest.apiId, + identifierOverride: + loadSnippetTemplateRequest.endpointId.identifierOverride, + sdkId: this.getSdkId(sdkFromRequest), }, - PUT: { - typescript: undefined, - python: undefined, + }); + } + if (snippetTemplate == null) { + snippetTemplate = await this.prisma.snippetTemplate.findFirst({ + where: { + orgId: loadSnippetTemplateRequest.orgId, + apiName: loadSnippetTemplateRequest.apiId, + endpointPath: loadSnippetTemplateRequest.endpointId?.path, + endpointMethod: loadSnippetTemplateRequest.endpointId?.method, + sdkId: this.getSdkId(sdkFromRequest), }, - GET: { - typescript: undefined, - python: undefined, + }); + } + + if (!snippetTemplate) { + return null; + } + return { + apiDefinitionId: FdrAPI.ApiDefinitionId(snippetTemplate.apiDefinitionId), + additionalTemplates: undefined, + endpointId: { + path: FdrAPI.EndpointPathLiteral(snippetTemplate.endpointPath), + method: snippetTemplate.endpointMethod, + identifierOverride: snippetTemplate.identifierOverride ?? undefined, + }, + sdk: sdkFromRequest, + snippetTemplate: { + type: snippetTemplate.version, + functionInvocation: readBuffer( + snippetTemplate.functionInvocation + ) as FdrAPI.Template, + clientInstantiation: readBuffer( + snippetTemplate.clientInstantiation + ) as string, + }, + }; + } + + private getSdkId(sdk: FdrAPI.Sdk): string { + switch (sdk.type) { + case "typescript": + return SdkIdFactory.fromTypescript(sdk); + case "python": + return SdkIdFactory.fromPython(sdk); + case "go": + return SdkIdFactory.fromGo(sdk); + case "ruby": + return SdkIdFactory.fromRuby(sdk); + case "java": + return SdkIdFactory.fromJava(sdk); + } + } + + public async storeSnippetTemplate({ + storeSnippetsInfo, + }: { + storeSnippetsInfo: FdrAPI.RegisterSnippetTemplateBatchRequest; + }): Promise { + const dbApi = await this.prisma.snippetApi.findUnique({ + where: { + orgId_apiName: { + orgId: storeSnippetsInfo.orgId, + apiName: storeSnippetsInfo.apiId, }, - DELETE: { - typescript: undefined, - python: undefined, + }, + }); + if (dbApi == null) { + await this.prisma.snippetApi.create({ + data: { + orgId: storeSnippetsInfo.orgId, + apiName: storeSnippetsInfo.apiId, }, - }; + }); + } + const sdkDao = new SdkDaoImpl(this.prisma); - public async loadSnippetTemplatesByEndpoint({ - orgId, - apiId, - sdkRequests, - definition, - }: { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - sdkRequests: FdrAPI.SdkRequest[]; - definition: APIV1Write.ApiDefinition; - }): Promise>> { - const endpoints: APIV1Write.EndpointDefinition[] = []; - for (const endpoint of definition.rootPackage.endpoints) { - endpoints.push(endpoint); - } + await this.prisma.$transaction(async (tx) => { + const snippets: Prisma.Enumerable< + WithoutQuestionMarks + > = []; + const sdks: Prisma.Enumerable = []; + await Promise.all( + storeSnippetsInfo.snippets.map(async (snippet) => { + const sdkId = this.getSdkId(snippet.sdk); - for (const subpackage of Object.values(definition.subpackages)) { - for (const endpoint of subpackage.endpoints) { - endpoints.push(endpoint); - } - } + snippets.push({ + id: uuidv4(), + orgId: storeSnippetsInfo.orgId, + apiName: storeSnippetsInfo.apiId, + apiDefinitionId: storeSnippetsInfo.apiDefinitionId, + endpointPath: snippet.endpointId.path, + endpointMethod: snippet.endpointId.method, + identifierOverride: snippet.endpointId.identifierOverride, + sdkId, + version: snippet.snippetTemplate.type, + functionInvocation: writeBuffer( + snippet.snippetTemplate.functionInvocation + ), + clientInstantiation: writeBuffer( + snippet.snippetTemplate.clientInstantiation + ), + }); - const toRet: Record< - FdrAPI.EndpointPathLiteral, - Record - > = {}; - for (const endpoint of endpoints) { - for (const sdk of sdkRequests) { - if (sdk.type !== "typescript" && sdk.type !== "python") { - continue; - } - const result = await this.loadSnippetTemplate({ - loadSnippetTemplateRequest: { - sdk, - orgId, - apiId, - endpointId: { - path: getEndpointPathAsString(endpoint), - method: endpoint.method, - identifierOverride: undefined, - }, - }, - }); - if (result != null) { - const value = { - ...(toRet[result.endpointId.path] ?? this.DEFAULT_ENDPOINT_SNIPPET_TEMPLATES), - [result.endpointId.method]: { - ...(toRet[result.endpointId.path]?.[result.endpointId.method] ?? {}), - [sdk.type]: result.snippetTemplate, - }, - }; + sdks.push({ + id: sdkId, + sdkPackage: getPackageNameFromSdkRequest(snippet.sdk), + version: snippet.sdk.version, + language: getLanguageFromRequest({ sdk: snippet.sdk }), + sdk: writeBuffer(snippet.sdk), + }); + }) + ); - toRet[result.endpointId.path] = value; - } - } + await sdkDao.createManySdks(sdks, tx); + await tx.snippetTemplate.createMany({ + data: snippets, + }); + }); + } + + public DEFAULT_ENDPOINT_SNIPPET_TEMPLATES: Record< + FdrAPI.HttpMethod, + APIV1Read.EndpointSnippetTemplates + > = { + PATCH: { + typescript: undefined, + python: undefined, + }, + POST: { + typescript: undefined, + python: undefined, + }, + PUT: { + typescript: undefined, + python: undefined, + }, + GET: { + typescript: undefined, + python: undefined, + }, + DELETE: { + typescript: undefined, + python: undefined, + }, + }; + + public async loadSnippetTemplatesByEndpoint({ + orgId, + apiId, + sdkRequests, + definition, + }: { + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + sdkRequests: FdrAPI.SdkRequest[]; + definition: APIV1Write.ApiDefinition; + }): Promise< + Record< + FdrAPI.EndpointPathLiteral, + Record + > + > { + const endpoints: APIV1Write.EndpointDefinition[] = []; + for (const endpoint of definition.rootPackage.endpoints) { + endpoints.push(endpoint); + } + + for (const subpackage of Object.values(definition.subpackages)) { + for (const endpoint of subpackage.endpoints) { + endpoints.push(endpoint); + } + } + + const toRet: Record< + FdrAPI.EndpointPathLiteral, + Record + > = {}; + for (const endpoint of endpoints) { + for (const sdk of sdkRequests) { + if (sdk.type !== "typescript" && sdk.type !== "python") { + continue; } + const result = await this.loadSnippetTemplate({ + loadSnippetTemplateRequest: { + sdk, + orgId, + apiId, + endpointId: { + path: getEndpointPathAsString(endpoint), + method: endpoint.method, + identifierOverride: undefined, + }, + }, + }); + if (result != null) { + const value = { + ...(toRet[result.endpointId.path] ?? + this.DEFAULT_ENDPOINT_SNIPPET_TEMPLATES), + [result.endpointId.method]: { + ...(toRet[result.endpointId.path]?.[result.endpointId.method] ?? + {}), + [sdk.type]: result.snippetTemplate, + }, + }; - return toRet; + toRet[result.endpointId.path] = value; + } + } } + + return toRet; + } } function getEndpointPathAsString(endpoint: APIV1Write.EndpointDefinition) { - let endpointPath = ""; - for (const part of endpoint.path.parts) { - if (part.type === "literal") { - endpointPath += part.value; - } else { - endpointPath += `{${part.value}}`; - } + let endpointPath = ""; + for (const part of endpoint.path.parts) { + if (part.type === "literal") { + endpointPath += part.value; + } else { + endpointPath += `{${part.value}}`; } - return APIV1Write.EndpointPathLiteral(endpointPath); + } + return APIV1Write.EndpointPathLiteral(endpointPath); } diff --git a/servers/fdr/src/db/snippets/SnippetsDao.ts b/servers/fdr/src/db/snippets/SnippetsDao.ts index 55faf94f37..2e08911bcf 100644 --- a/servers/fdr/src/db/snippets/SnippetsDao.ts +++ b/servers/fdr/src/db/snippets/SnippetsDao.ts @@ -7,410 +7,468 @@ import { SdkDaoImpl } from "../sdk/SdkDao"; import { PrismaTransaction, SdkId } from "../types"; import { EndpointSnippetCollector } from "./EndpointSnippetCollectors"; import { SdkIdFactory } from "./SdkIdFactory"; -import { getPackageNameFromSdkSnippetsCreate, getSdkFromSdkRequest } from "./getPackageNameFromSdkSnippetsCreate"; +import { + getPackageNameFromSdkSnippetsCreate, + getSdkFromSdkRequest, +} from "./getPackageNameFromSdkSnippetsCreate"; export const DEFAULT_SNIPPETS_PAGE_SIZE = 100; export interface LoadDbSnippetsPage { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - endpointIdentifier: FdrAPI.EndpointIdentifier | undefined; - exampleIdentifier: string | undefined; - sdks: FdrAPI.SdkRequest[] | undefined; - page: number | undefined; + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + endpointIdentifier: FdrAPI.EndpointIdentifier | undefined; + exampleIdentifier: string | undefined; + sdks: FdrAPI.SdkRequest[] | undefined; + page: number | undefined; } export interface DbSnippetsPage { - snippets: Record; - snippetsByEndpointId: Record; - nextPage: number | undefined; + snippets: Record; + snippetsByEndpointId: Record; + nextPage: number | undefined; } export interface StoreSnippetsInfo { - orgId: FdrAPI.OrgId; - apiId: FdrAPI.ApiId; - sdk: FdrAPI.SdkSnippetsCreate; + orgId: FdrAPI.OrgId; + apiId: FdrAPI.ApiId; + sdk: FdrAPI.SdkSnippetsCreate; } export interface StoreSnippetsResponse { - sdkId: string; + sdkId: string; } export interface SdkInfo { - id: string; - language: Language; + id: string; + language: Language; } export interface SnippetsDao { - // TODO(armando): whenever we call this, we should call this other endpoint too - loadAllSnippetsForSdkIds( - sdkIds: string[], - ): Promise>>; + // TODO(armando): whenever we call this, we should call this other endpoint too + loadAllSnippetsForSdkIds( + sdkIds: string[] + ): Promise< + Record< + string, + Record + > + >; - loadAllSnippetsForSdkIdsByEndpointId(sdkIds: string[]): Promise>>; + loadAllSnippetsForSdkIdsByEndpointId( + sdkIds: string[] + ): Promise>>; - // TODO(armando): same here - loadAllSnippetsBySdkId(sdkId: string): Promise>; + // TODO(armando): same here + loadAllSnippetsBySdkId( + sdkId: string + ): Promise< + Record + >; - loadAllSnippetsBySdkIdByEndpointId(sdkId: string): Promise>; + loadAllSnippetsBySdkIdByEndpointId( + sdkId: string + ): Promise>; - loadSnippetsPage({ loadSnippetsInfo }: { loadSnippetsInfo: LoadDbSnippetsPage }): Promise; + loadSnippetsPage({ + loadSnippetsInfo, + }: { + loadSnippetsInfo: LoadDbSnippetsPage; + }): Promise; - storeSnippets({ storeSnippetsInfo }: { storeSnippetsInfo: StoreSnippetsInfo }): Promise; + storeSnippets({ + storeSnippetsInfo, + }: { + storeSnippetsInfo: StoreSnippetsInfo; + }): Promise; } export class SnippetsDaoImpl implements SnippetsDao { - constructor(private readonly prisma: PrismaClient) {} - private async loadAllSnippetsToCollector(sdkId: string): Promise { - const dbSdkRow = await this.prisma.sdk.findFirst({ - where: { - id: { - in: [sdkId], - }, - }, - }); - if (dbSdkRow == null) { - throw new InternalError(`Internal error; SDK identified by ${sdkId} was not found`); - } - const dbSnippetRows = await this.prisma.snippet.findMany({ - where: { - sdkId, - }, + constructor(private readonly prisma: PrismaClient) {} + private async loadAllSnippetsToCollector( + sdkId: string + ): Promise { + const dbSdkRow = await this.prisma.sdk.findFirst({ + where: { + id: { + in: [sdkId], + }, + }, + }); + if (dbSdkRow == null) { + throw new InternalError( + `Internal error; SDK identified by ${sdkId} was not found` + ); + } + const dbSnippetRows = await this.prisma.snippet.findMany({ + where: { + sdkId, + }, + }); + const snippetCollector = new EndpointSnippetCollector(); + for (const dbSnippetRow of dbSnippetRows) { + const snippet = convertSnippetFromDb({ + dbSdkRow, + dbSnippet: dbSnippetRow, + }); + if (snippet != null) { + snippetCollector.collect({ + endpointPath: FdrAPI.EndpointPathLiteral(dbSnippetRow.endpointPath), + endpointMethod: dbSnippetRow.endpointMethod, + identifierOverride: dbSnippetRow.identifierOverride ?? undefined, + snippet, }); - const snippetCollector = new EndpointSnippetCollector(); - for (const dbSnippetRow of dbSnippetRows) { - const snippet = convertSnippetFromDb({ - dbSdkRow, - dbSnippet: dbSnippetRow, - }); - if (snippet != null) { - snippetCollector.collect({ - endpointPath: FdrAPI.EndpointPathLiteral(dbSnippetRow.endpointPath), - endpointMethod: dbSnippetRow.endpointMethod, - identifierOverride: dbSnippetRow.identifierOverride ?? undefined, - snippet, - }); - } - } - - return snippetCollector; + } } - public async loadAllSnippetsBySdkIdByEndpointId(sdkId: string): Promise> { - const snippetCollector = await this.loadAllSnippetsToCollector(sdkId); - return snippetCollector.getByIdentifierOverride(); - } + return snippetCollector; + } - public async loadAllSnippetsForSdkIdsByEndpointId( - sdkIds: string[], - ): Promise>> { - const result: Record> = {}; - for (const sdkId of sdkIds) { - const snippets = await this.loadAllSnippetsBySdkIdByEndpointId(sdkId); - result[sdkId] = snippets; - } - return result; - } + public async loadAllSnippetsBySdkIdByEndpointId( + sdkId: string + ): Promise> { + const snippetCollector = await this.loadAllSnippetsToCollector(sdkId); + return snippetCollector.getByIdentifierOverride(); + } - public async loadAllSnippetsForSdkIds( - sdkIds: string[], - ): Promise>> { - const result: Record> = {}; - for (const sdkId of sdkIds) { - const snippets = await this.loadAllSnippetsBySdkId(sdkId); - result[sdkId] = snippets; - } - return result; + public async loadAllSnippetsForSdkIdsByEndpointId( + sdkIds: string[] + ): Promise>> { + const result: Record> = {}; + for (const sdkId of sdkIds) { + const snippets = await this.loadAllSnippetsBySdkIdByEndpointId(sdkId); + result[sdkId] = snippets; } + return result; + } - public async loadAllSnippetsBySdkId(sdkId: string): Promise> { - const snippetCollector = await this.loadAllSnippetsToCollector(sdkId); - return snippetCollector.get(); + public async loadAllSnippetsForSdkIds( + sdkIds: string[] + ): Promise>> { + const result: Record< + string, + Record + > = {}; + for (const sdkId of sdkIds) { + const snippets = await this.loadAllSnippetsBySdkId(sdkId); + result[sdkId] = snippets; } + return result; + } + + public async loadAllSnippetsBySdkId( + sdkId: string + ): Promise> { + const snippetCollector = await this.loadAllSnippetsToCollector(sdkId); + return snippetCollector.get(); + } - public async loadSnippetsPage({ - loadSnippetsInfo, - }: { - loadSnippetsInfo: LoadDbSnippetsPage; - }): Promise { - return await this.prisma.$transaction(async (tx) => { - const sdkIds: string[] | undefined = - loadSnippetsInfo.sdks != null - ? await Promise.all( - loadSnippetsInfo.sdks.map(async (sdk: FdrAPI.SdkRequest) => { - return sdkInfoFromSdk({ - sdk: await getSdkFromSdkRequest(this.prisma, sdk), - }).id; - }), - ) - : undefined; + public async loadSnippetsPage({ + loadSnippetsInfo, + }: { + loadSnippetsInfo: LoadDbSnippetsPage; + }): Promise { + return await this.prisma.$transaction(async (tx) => { + const sdkIds: string[] | undefined = + loadSnippetsInfo.sdks != null + ? await Promise.all( + loadSnippetsInfo.sdks.map(async (sdk: FdrAPI.SdkRequest) => { + return sdkInfoFromSdk({ + sdk: await getSdkFromSdkRequest(this.prisma, sdk), + }).id; + }) + ) + : undefined; - const sdkIdsForSnippets = - sdkIds != null ? sdkIds : await this.getSdkIdsReferencedBySnippetRows({ loadSnippetsInfo, tx }); - const dbSdkRows = await tx.sdk.findMany({ - where: { - id: { - in: sdkIdsForSnippets, - }, - }, + const sdkIdsForSnippets = + sdkIds != null + ? sdkIds + : await this.getSdkIdsReferencedBySnippetRows({ + loadSnippetsInfo, + tx, }); - const sdkIdToDbSdkRow = Object.fromEntries(dbSdkRows.map((dbSdkRow) => [dbSdkRow.id, dbSdkRow])); + const dbSdkRows = await tx.sdk.findMany({ + where: { + id: { + in: sdkIdsForSnippets, + }, + }, + }); + const sdkIdToDbSdkRow = Object.fromEntries( + dbSdkRows.map((dbSdkRow) => [dbSdkRow.id, dbSdkRow]) + ); - const loadSnippetsQuery: Prisma.SnippetFindManyArgs = { - where: { - orgId: loadSnippetsInfo.orgId, - apiName: loadSnippetsInfo.apiId, - sdkId: { - in: sdkIds != null && sdkIds.length > 0 ? sdkIds : undefined, - }, - endpointPath: loadSnippetsInfo.endpointIdentifier?.path, - endpointMethod: loadSnippetsInfo.endpointIdentifier?.method, - identifierOverride: loadSnippetsInfo.endpointIdentifier?.identifierOverride, - exampleIdentifier: loadSnippetsInfo.exampleIdentifier, - }, - orderBy: { - createdAt: "desc", - }, - take: DEFAULT_SNIPPETS_PAGE_SIZE, - skip: - loadSnippetsInfo.page != null - ? (loadSnippetsInfo.page - 1) * DEFAULT_SNIPPETS_PAGE_SIZE - : undefined, - }; - const snippetDbRows = await tx.snippet.findMany(loadSnippetsQuery); + const loadSnippetsQuery: Prisma.SnippetFindManyArgs = { + where: { + orgId: loadSnippetsInfo.orgId, + apiName: loadSnippetsInfo.apiId, + sdkId: { + in: sdkIds != null && sdkIds.length > 0 ? sdkIds : undefined, + }, + endpointPath: loadSnippetsInfo.endpointIdentifier?.path, + endpointMethod: loadSnippetsInfo.endpointIdentifier?.method, + identifierOverride: + loadSnippetsInfo.endpointIdentifier?.identifierOverride, + exampleIdentifier: loadSnippetsInfo.exampleIdentifier, + }, + orderBy: { + createdAt: "desc", + }, + take: DEFAULT_SNIPPETS_PAGE_SIZE, + skip: + loadSnippetsInfo.page != null + ? (loadSnippetsInfo.page - 1) * DEFAULT_SNIPPETS_PAGE_SIZE + : undefined, + }; + const snippetDbRows = await tx.snippet.findMany(loadSnippetsQuery); - const snippetCollector = new EndpointSnippetCollector(); - for (const dbSnippetRow of snippetDbRows) { - const dbSdkRow = sdkIdToDbSdkRow[dbSnippetRow.sdkId]; - if (dbSdkRow == null) { - throw new InternalError(`Internal error; SDK identified by ${dbSnippetRow.sdkId} was not found`); - } - const snippet = convertSnippetFromDb({ - dbSdkRow, - dbSnippet: dbSnippetRow, - }); - if (snippet != null) { - snippetCollector.collect({ - endpointPath: FdrAPI.EndpointPathLiteral(dbSnippetRow.endpointPath), - endpointMethod: dbSnippetRow.endpointMethod, - identifierOverride: dbSnippetRow.identifierOverride ?? undefined, - snippet, - }); - } - } - return { - nextPage: - snippetDbRows.length === DEFAULT_SNIPPETS_PAGE_SIZE ? (loadSnippetsInfo.page ?? 1) + 1 : undefined, - snippets: snippetCollector.get(), - snippetsByEndpointId: snippetCollector.getByIdentifierOverride(), - }; + const snippetCollector = new EndpointSnippetCollector(); + for (const dbSnippetRow of snippetDbRows) { + const dbSdkRow = sdkIdToDbSdkRow[dbSnippetRow.sdkId]; + if (dbSdkRow == null) { + throw new InternalError( + `Internal error; SDK identified by ${dbSnippetRow.sdkId} was not found` + ); + } + const snippet = convertSnippetFromDb({ + dbSdkRow, + dbSnippet: dbSnippetRow, }); - } + if (snippet != null) { + snippetCollector.collect({ + endpointPath: FdrAPI.EndpointPathLiteral(dbSnippetRow.endpointPath), + endpointMethod: dbSnippetRow.endpointMethod, + identifierOverride: dbSnippetRow.identifierOverride ?? undefined, + snippet, + }); + } + } + return { + nextPage: + snippetDbRows.length === DEFAULT_SNIPPETS_PAGE_SIZE + ? (loadSnippetsInfo.page ?? 1) + 1 + : undefined, + snippets: snippetCollector.get(), + snippetsByEndpointId: snippetCollector.getByIdentifierOverride(), + }; + }); + } - public async getSdkIdsReferencedBySnippetRows({ - tx, - loadSnippetsInfo, - }: { - tx: PrismaTransaction; - loadSnippetsInfo: LoadDbSnippetsPage; - }): Promise { - return ( - await tx.snippet.groupBy({ - by: ["sdkId"], - where: { - orgId: loadSnippetsInfo.orgId, - apiName: loadSnippetsInfo.apiId, - endpointPath: loadSnippetsInfo.endpointIdentifier?.path, - endpointMethod: loadSnippetsInfo.endpointIdentifier?.method, - identifierOverride: loadSnippetsInfo.endpointIdentifier?.identifierOverride, - exampleIdentifier: loadSnippetsInfo.exampleIdentifier, - }, - }) - ).map((row) => row.sdkId); - } + public async getSdkIdsReferencedBySnippetRows({ + tx, + loadSnippetsInfo, + }: { + tx: PrismaTransaction; + loadSnippetsInfo: LoadDbSnippetsPage; + }): Promise { + return ( + await tx.snippet.groupBy({ + by: ["sdkId"], + where: { + orgId: loadSnippetsInfo.orgId, + apiName: loadSnippetsInfo.apiId, + endpointPath: loadSnippetsInfo.endpointIdentifier?.path, + endpointMethod: loadSnippetsInfo.endpointIdentifier?.method, + identifierOverride: + loadSnippetsInfo.endpointIdentifier?.identifierOverride, + exampleIdentifier: loadSnippetsInfo.exampleIdentifier, + }, + }) + ).map((row) => row.sdkId); + } - public async storeSnippets({ - storeSnippetsInfo, - }: { - storeSnippetsInfo: StoreSnippetsInfo; - }): Promise { - const sdkDao = new SdkDaoImpl(this.prisma); + public async storeSnippets({ + storeSnippetsInfo, + }: { + storeSnippetsInfo: StoreSnippetsInfo; + }): Promise { + const sdkDao = new SdkDaoImpl(this.prisma); - return await this.prisma.$transaction(async (tx) => { - const dbApi = await tx.snippetApi.findUnique({ - where: { - orgId_apiName: { - orgId: storeSnippetsInfo.orgId, - apiName: storeSnippetsInfo.apiId, - }, - }, - }); - if (dbApi === null) { - await tx.snippetApi.create({ - data: { - orgId: storeSnippetsInfo.orgId, - apiName: storeSnippetsInfo.apiId, - }, - }); - } - const sdkInfo = sdkInfoFromSnippetsCreate({ - sdkSnippetsCreate: storeSnippetsInfo.sdk, - }); + return await this.prisma.$transaction(async (tx) => { + const dbApi = await tx.snippetApi.findUnique({ + where: { + orgId_apiName: { + orgId: storeSnippetsInfo.orgId, + apiName: storeSnippetsInfo.apiId, + }, + }, + }); + if (dbApi === null) { + await tx.snippetApi.create({ + data: { + orgId: storeSnippetsInfo.orgId, + apiName: storeSnippetsInfo.apiId, + }, + }); + } + const sdkInfo = sdkInfoFromSnippetsCreate({ + sdkSnippetsCreate: storeSnippetsInfo.sdk, + }); - await sdkDao.createSdkIfNotExists( - { - id: sdkInfo.id, - sdkPackage: getPackageNameFromSdkSnippetsCreate(storeSnippetsInfo.sdk), - version: storeSnippetsInfo.sdk.sdk.version, - language: sdkInfo.language, - sdk: writeBuffer(storeSnippetsInfo.sdk.sdk), - }, - tx, - true, - ); + await sdkDao.createSdkIfNotExists( + { + id: sdkInfo.id, + sdkPackage: getPackageNameFromSdkSnippetsCreate( + storeSnippetsInfo.sdk + ), + version: storeSnippetsInfo.sdk.sdk.version, + language: sdkInfo.language, + sdk: writeBuffer(storeSnippetsInfo.sdk.sdk), + }, + tx, + true + ); - const snippets: Prisma.Enumerable = []; - storeSnippetsInfo.sdk.snippets.map((snippet) => { - snippets.push({ - id: uuidv4(), - orgId: storeSnippetsInfo.orgId, - apiName: storeSnippetsInfo.apiId, - endpointPath: snippet.endpoint.path, - endpointMethod: snippet.endpoint.method, - identifierOverride: snippet.endpoint.identifierOverride, - exampleIdentifier: snippet.exampleIdentifier, - sdkId: sdkInfo.id, - snippet: writeBuffer(snippet.snippet), - }); - }); - await tx.snippet.createMany({ - data: snippets, - }); - return { - sdkId: sdkInfo.id, - }; + const snippets: Prisma.Enumerable = []; + storeSnippetsInfo.sdk.snippets.map((snippet) => { + snippets.push({ + id: uuidv4(), + orgId: storeSnippetsInfo.orgId, + apiName: storeSnippetsInfo.apiId, + endpointPath: snippet.endpoint.path, + endpointMethod: snippet.endpoint.method, + identifierOverride: snippet.endpoint.identifierOverride, + exampleIdentifier: snippet.exampleIdentifier, + sdkId: sdkInfo.id, + snippet: writeBuffer(snippet.snippet), }); - } + }); + await tx.snippet.createMany({ + data: snippets, + }); + return { + sdkId: sdkInfo.id, + }; + }); + } } -function sdkInfoFromSnippetsCreate({ sdkSnippetsCreate }: { sdkSnippetsCreate: FdrAPI.SdkSnippetsCreate }): SdkInfo { - switch (sdkSnippetsCreate.type) { - case "typescript": - return { - language: Language.TYPESCRIPT, - id: SdkIdFactory.fromTypescript(sdkSnippetsCreate.sdk), - }; - case "python": - return { - language: Language.PYTHON, - id: SdkIdFactory.fromPython(sdkSnippetsCreate.sdk), - }; - case "go": - return { - language: Language.GO, - id: SdkIdFactory.fromGo(sdkSnippetsCreate.sdk), - }; - case "ruby": - return { - language: Language.RUBY, - id: SdkIdFactory.fromRuby(sdkSnippetsCreate.sdk), - }; - case "java": - return { - language: Language.JAVA, - id: SdkIdFactory.fromJava(sdkSnippetsCreate.sdk), - }; - } +function sdkInfoFromSnippetsCreate({ + sdkSnippetsCreate, +}: { + sdkSnippetsCreate: FdrAPI.SdkSnippetsCreate; +}): SdkInfo { + switch (sdkSnippetsCreate.type) { + case "typescript": + return { + language: Language.TYPESCRIPT, + id: SdkIdFactory.fromTypescript(sdkSnippetsCreate.sdk), + }; + case "python": + return { + language: Language.PYTHON, + id: SdkIdFactory.fromPython(sdkSnippetsCreate.sdk), + }; + case "go": + return { + language: Language.GO, + id: SdkIdFactory.fromGo(sdkSnippetsCreate.sdk), + }; + case "ruby": + return { + language: Language.RUBY, + id: SdkIdFactory.fromRuby(sdkSnippetsCreate.sdk), + }; + case "java": + return { + language: Language.JAVA, + id: SdkIdFactory.fromJava(sdkSnippetsCreate.sdk), + }; + } } function sdkInfoFromSdk({ sdk }: { sdk: FdrAPI.Sdk }): SdkInfo { - switch (sdk.type) { - case "typescript": - return { - language: Language.TYPESCRIPT, - id: SdkIdFactory.fromTypescript(sdk), - }; - case "python": - return { - language: Language.PYTHON, - id: SdkIdFactory.fromPython(sdk), - }; - case "go": - return { - language: Language.GO, - id: SdkIdFactory.fromGo(sdk), - }; - case "ruby": - return { - language: Language.RUBY, - id: SdkIdFactory.fromRuby(sdk), - }; - case "java": - return { - language: Language.JAVA, - id: SdkIdFactory.fromJava(sdk), - }; - } + switch (sdk.type) { + case "typescript": + return { + language: Language.TYPESCRIPT, + id: SdkIdFactory.fromTypescript(sdk), + }; + case "python": + return { + language: Language.PYTHON, + id: SdkIdFactory.fromPython(sdk), + }; + case "go": + return { + language: Language.GO, + id: SdkIdFactory.fromGo(sdk), + }; + case "ruby": + return { + language: Language.RUBY, + id: SdkIdFactory.fromRuby(sdk), + }; + case "java": + return { + language: Language.JAVA, + id: SdkIdFactory.fromJava(sdk), + }; + } } function convertSnippetFromDb({ - dbSdkRow, - dbSnippet, + dbSdkRow, + dbSnippet, }: { - dbSdkRow: Sdk; - dbSnippet: Snippet; + dbSdkRow: Sdk; + dbSnippet: Snippet; }): FdrAPI.Snippet | undefined { - const sdk = readBuffer(dbSdkRow.sdk) as FdrAPI.Sdk; - switch (dbSdkRow.language) { - case Language.TYPESCRIPT: - return { - type: "typescript", - sdk: sdk as FdrAPI.TypeScriptSdk, - client: (readBuffer(dbSnippet.snippet) as FdrAPI.TypeScriptSnippetCode).client, - exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, - }; - case Language.PYTHON: { - const pythonSnippetCode: FdrAPI.PythonSnippetCode = readBuffer( - dbSnippet.snippet, - ) as FdrAPI.PythonSnippetCode; - return { - type: "python", - sdk: sdk as FdrAPI.PythonSdk, - async_client: pythonSnippetCode.async_client, - sync_client: pythonSnippetCode.sync_client, - exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, - }; - } - case Language.GO: - return { - type: "go", - sdk: sdk as FdrAPI.GoSdk, - client: (readBuffer(dbSnippet.snippet) as FdrAPI.GoSnippetCode).client, - exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, - }; - case Language.RUBY: - return { - type: "ruby", - sdk: sdk as FdrAPI.RubySdk, - client: (readBuffer(dbSnippet.snippet) as FdrAPI.RubySnippetCode).client, - exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, - }; - case Language.JAVA: { - const javaSnippetCode: FdrAPI.JavaSnippetCode = readBuffer(dbSnippet.snippet) as FdrAPI.JavaSnippetCode; - return { - type: "java", - sdk: sdk as FdrAPI.JavaSdk, - async_client: javaSnippetCode.async_client, - sync_client: javaSnippetCode.sync_client, - exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, - }; - } - case Language.CSHARP: - case Language.PHP: - case Language.SWIFT: - case Language.RUST: - return undefined; - default: - assertNever(dbSdkRow.language); + const sdk = readBuffer(dbSdkRow.sdk) as FdrAPI.Sdk; + switch (dbSdkRow.language) { + case Language.TYPESCRIPT: + return { + type: "typescript", + sdk: sdk as FdrAPI.TypeScriptSdk, + client: (readBuffer(dbSnippet.snippet) as FdrAPI.TypeScriptSnippetCode) + .client, + exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, + }; + case Language.PYTHON: { + const pythonSnippetCode: FdrAPI.PythonSnippetCode = readBuffer( + dbSnippet.snippet + ) as FdrAPI.PythonSnippetCode; + return { + type: "python", + sdk: sdk as FdrAPI.PythonSdk, + async_client: pythonSnippetCode.async_client, + sync_client: pythonSnippetCode.sync_client, + exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, + }; + } + case Language.GO: + return { + type: "go", + sdk: sdk as FdrAPI.GoSdk, + client: (readBuffer(dbSnippet.snippet) as FdrAPI.GoSnippetCode).client, + exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, + }; + case Language.RUBY: + return { + type: "ruby", + sdk: sdk as FdrAPI.RubySdk, + client: (readBuffer(dbSnippet.snippet) as FdrAPI.RubySnippetCode) + .client, + exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, + }; + case Language.JAVA: { + const javaSnippetCode: FdrAPI.JavaSnippetCode = readBuffer( + dbSnippet.snippet + ) as FdrAPI.JavaSnippetCode; + return { + type: "java", + sdk: sdk as FdrAPI.JavaSdk, + async_client: javaSnippetCode.async_client, + sync_client: javaSnippetCode.sync_client, + exampleIdentifier: dbSnippet.exampleIdentifier ?? undefined, + }; } + case Language.CSHARP: + case Language.PHP: + case Language.SWIFT: + case Language.RUST: + return undefined; + default: + assertNever(dbSdkRow.language); + } } diff --git a/servers/fdr/src/db/snippets/getPackageNameFromSdkSnippetsCreate.ts b/servers/fdr/src/db/snippets/getPackageNameFromSdkSnippetsCreate.ts index 318d414cf4..58707f777d 100644 --- a/servers/fdr/src/db/snippets/getPackageNameFromSdkSnippetsCreate.ts +++ b/servers/fdr/src/db/snippets/getPackageNameFromSdkSnippetsCreate.ts @@ -3,83 +3,92 @@ import { Language, PrismaClient } from "@prisma/client"; import { BadRequestError, Sdk, SdkRequest } from "../../api/generated/api"; import { assertNever } from "../../util"; -export function getPackageNameFromSdkSnippetsCreate(create: FdrAPI.SdkSnippetsCreate): string { - switch (create.type) { - case "go": - return create.sdk.githubRepo; - case "java": - return `${create.sdk.group}:${create.sdk.artifact}`; - case "python": - return create.sdk.package; - case "typescript": - return create.sdk.package; - case "ruby": - return create.sdk.gem; - default: - assertNever(create); - } +export function getPackageNameFromSdkSnippetsCreate( + create: FdrAPI.SdkSnippetsCreate +): string { + switch (create.type) { + case "go": + return create.sdk.githubRepo; + case "java": + return `${create.sdk.group}:${create.sdk.artifact}`; + case "python": + return create.sdk.package; + case "typescript": + return create.sdk.package; + case "ruby": + return create.sdk.gem; + default: + assertNever(create); + } } export function getPackageNameFromSdkRequest(sdk: FdrAPI.SdkRequest): string { - switch (sdk.type) { - case "go": - return sdk.githubRepo; - case "java": - return `${sdk.group}:${sdk.artifact}`; - case "python": - return sdk.package; - case "typescript": - return sdk.package; - case "ruby": - return sdk.gem; - default: - assertNever(sdk); - } + switch (sdk.type) { + case "go": + return sdk.githubRepo; + case "java": + return `${sdk.group}:${sdk.artifact}`; + case "python": + return sdk.package; + case "typescript": + return sdk.package; + case "ruby": + return sdk.gem; + default: + assertNever(sdk); + } } -export async function getSdkFromSdkRequest(prismaClient: PrismaClient, request: SdkRequest): Promise { - if (request.version != null) { - return { ...request, version: request.version }; - } else { - const packageName = getPackageNameFromSdkRequest(request); - const language = getLanguageFromRequest({ sdk: request }); - const sdkDao = await prismaClient.sdk.findFirst({ - select: { - version: true, - }, - where: { - package: packageName, - language, - }, - orderBy: { - createdAt: "desc", - }, - }); - - if (sdkDao == null) { - throw new BadRequestError(`No SDK found for the given request: ${language} ${packageName}`); - } else if (sdkDao.version == null) { - throw new BadRequestError("No version for SDK found for the given request"); - } +export async function getSdkFromSdkRequest( + prismaClient: PrismaClient, + request: SdkRequest +): Promise { + if (request.version != null) { + return { ...request, version: request.version }; + } else { + const packageName = getPackageNameFromSdkRequest(request); + const language = getLanguageFromRequest({ sdk: request }); + const sdkDao = await prismaClient.sdk.findFirst({ + select: { + version: true, + }, + where: { + package: packageName, + language, + }, + orderBy: { + createdAt: "desc", + }, + }); - return { - ...request, - version: sdkDao.version, - }; + if (sdkDao == null) { + throw new BadRequestError( + `No SDK found for the given request: ${language} ${packageName}` + ); + } else if (sdkDao.version == null) { + throw new BadRequestError( + "No version for SDK found for the given request" + ); } + + return { + ...request, + version: sdkDao.version, + }; + } } export function getLanguageFromRequest({ sdk }: { sdk: SdkRequest }): Language { - switch (sdk.type) { - case "typescript": - return Language.TYPESCRIPT; - case "python": - return Language.PYTHON; - case "go": - return Language.GO; - case "ruby": - return Language.RUBY; - case "java": - return Language.JAVA; - } + switch (sdk.type) { + case "typescript": + return Language.TYPESCRIPT; + case "python": + return Language.PYTHON; + case "go": + return Language.GO; + case "ruby": + return Language.RUBY; + case "java": + return Language.JAVA; + } } diff --git a/servers/fdr/src/db/types.ts b/servers/fdr/src/db/types.ts index ac11882e8d..3bb4385d0d 100644 --- a/servers/fdr/src/db/types.ts +++ b/servers/fdr/src/db/types.ts @@ -7,8 +7,8 @@ export type IndexSegmentIds = string[]; // Prisma Transaction Type export type PrismaTransaction = Omit< - PrismaClient, - "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" + PrismaClient, + "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" >; export type SdkId = string; diff --git a/servers/fdr/src/healthchecks/checkRedis.ts b/servers/fdr/src/healthchecks/checkRedis.ts index 01fe4c739f..162497b9c9 100644 --- a/servers/fdr/src/healthchecks/checkRedis.ts +++ b/servers/fdr/src/healthchecks/checkRedis.ts @@ -6,76 +6,83 @@ import RedisDocsDefinitionStore from "../services/docs-cache/RedisDocsDefinition const HEALTHCHECK_KEY = "https://healthcheck.buildwithfern.com"; const HEALTHCHECK_DOCS_RESPONSE: CachedDocsResponse = { - dbFiles: {}, - isPrivate: true, - response: { - baseUrl: { - domain: "healthcheck.buildwithfern.com", - basePath: undefined, + dbFiles: {}, + isPrivate: true, + response: { + baseUrl: { + domain: "healthcheck.buildwithfern.com", + basePath: undefined, + }, + definition: { + pages: {}, + apis: {}, + config: { + navigation: { + items: [], + landingPage: undefined, }, - definition: { - pages: {}, - apis: {}, - config: { - navigation: { - items: [], - landingPage: undefined, - }, - root: undefined, - title: undefined, - defaultLanguage: undefined, - announcement: undefined, - navbarLinks: undefined, - footerLinks: undefined, - logoHeight: undefined, - logoHref: undefined, - favicon: undefined, - metadata: undefined, - redirects: undefined, - colorsV3: undefined, - layout: undefined, - typographyV2: undefined, - analyticsConfig: undefined, - integrations: undefined, - css: undefined, - js: undefined, - }, - files: {}, - filesV2: {}, - search: { - type: "singleAlgoliaIndex", - value: { - type: "unversioned", - indexSegment: { - id: FdrAPI.IndexSegmentId("healthcheck"), - searchApiKey: "dummy", - }, - }, - }, - algoliaSearchIndex: undefined, - jsFiles: undefined, - id: undefined, + root: undefined, + title: undefined, + defaultLanguage: undefined, + announcement: undefined, + navbarLinks: undefined, + footerLinks: undefined, + logoHeight: undefined, + logoHref: undefined, + favicon: undefined, + metadata: undefined, + redirects: undefined, + colorsV3: undefined, + layout: undefined, + typographyV2: undefined, + analyticsConfig: undefined, + integrations: undefined, + css: undefined, + js: undefined, + }, + files: {}, + filesV2: {}, + search: { + type: "singleAlgoliaIndex", + value: { + type: "unversioned", + indexSegment: { + id: FdrAPI.IndexSegmentId("healthcheck"), + searchApiKey: "dummy", + }, }, - lightModeEnabled: true, - orgId: OrgId("fern"), + }, + algoliaSearchIndex: undefined, + jsFiles: undefined, + id: undefined, }, - updatedTime: new Date(), - version: "v3", + lightModeEnabled: true, + orgId: OrgId("fern"), + }, + updatedTime: new Date(), + version: "v3", }; -export async function checkRedis({ redis }: { redis: RedisDocsDefinitionStore }): Promise { - try { - const healthcheckURL = new URL(HEALTHCHECK_KEY); - await redis.set({ url: healthcheckURL, value: HEALTHCHECK_DOCS_RESPONSE }); - const record = await redis.get({ url: healthcheckURL }); - - if (record?.response.baseUrl.domain !== healthcheckURL.hostname) { - return false; - } +export async function checkRedis({ + redis, +}: { + redis: RedisDocsDefinitionStore; +}): Promise { + try { + const healthcheckURL = new URL(HEALTHCHECK_KEY); + await redis.set({ url: healthcheckURL, value: HEALTHCHECK_DOCS_RESPONSE }); + const record = await redis.get({ url: healthcheckURL }); - return true; - } catch (err) { - LOGGER.error("Encountered error while retrieving and storing redis entries", err); - return false; + if (record?.response.baseUrl.domain !== healthcheckURL.hostname) { + return false; } + + return true; + } catch (err) { + LOGGER.error( + "Encountered error while retrieving and storing redis entries", + err + ); + return false; + } } diff --git a/servers/fdr/src/server.ts b/servers/fdr/src/server.ts index 73131884e8..7c626b8e41 100644 --- a/servers/fdr/src/server.ts +++ b/servers/fdr/src/server.ts @@ -35,20 +35,22 @@ expressApp.disable("x-powered-by"); // ========= Init Sentry ========= Sentry.init({ - dsn: "https://ca7d28b81fee41961a6f9f3fb59dfa8a@o4507138224160768.ingest.us.sentry.io/4507148234522624", - integrations: [ - // enable HTTP calls tracing - new Sentry.Integrations.Http({ tracing: true }), - // enable Express.js middleware tracing - new Sentry.Integrations.Express({ app: expressApp }), - nodeProfilingIntegration(), - ], - // Performance Monitoring - tracesSampleRate: config.applicationEnvironment == "prod" ? 0.25 : 0.1, // Capture 25% of the transactions - profilesSampleRate: config.applicationEnvironment == "prod" ? 0.25 : 0.1, - environment: config.applicationEnvironment, - maxValueLength: 1000, - enabled: config.applicationEnvironment === "dev" || config.applicationEnvironment == "prod", + dsn: "https://ca7d28b81fee41961a6f9f3fb59dfa8a@o4507138224160768.ingest.us.sentry.io/4507148234522624", + integrations: [ + // enable HTTP calls tracing + new Sentry.Integrations.Http({ tracing: true }), + // enable Express.js middleware tracing + new Sentry.Integrations.Express({ app: expressApp }), + nodeProfilingIntegration(), + ], + // Performance Monitoring + tracesSampleRate: config.applicationEnvironment == "prod" ? 0.25 : 0.1, // Capture 25% of the transactions + profilesSampleRate: config.applicationEnvironment == "prod" ? 0.25 : 0.1, + environment: config.applicationEnvironment, + maxValueLength: 1000, + enabled: + config.applicationEnvironment === "dev" || + config.applicationEnvironment == "prod", }); // The request handler must be the first middleware on the app @@ -65,78 +67,84 @@ setGlobalDispatcher(new Agent({ connect: { timeout: 5_000 } })); const app = new FdrApplication(config); expressApp.get("/health", async (_req, res) => { - const cacheInitialized = app.docsDefinitionCache.isInitialized(); - if (!cacheInitialized) { - app.logger.error("The docs definition cache is not initilialized. Erroring the health check."); - res.sendStatus(500); + const cacheInitialized = app.docsDefinitionCache.isInitialized(); + if (!cacheInitialized) { + app.logger.error( + "The docs definition cache is not initilialized. Erroring the health check." + ); + res.sendStatus(500); + } + if (app.redisDatastore != null) { + const redisHealthCheckSuccessful = await checkRedis({ + redis: app.redisDatastore, + }); + if (!redisHealthCheckSuccessful) { + app.logger.error( + "Records cannot be successfully written and read from redis" + ); + res.sendStatus(500); } - if (app.redisDatastore != null) { - const redisHealthCheckSuccessful = await checkRedis({ redis: app.redisDatastore }); - if (!redisHealthCheckSuccessful) { - app.logger.error("Records cannot be successfully written and read from redis"); - res.sendStatus(500); - } - } - res.sendStatus(200); + } + res.sendStatus(200); }); void startServer(); async function startServer(): Promise { - try { - await app.initialize(); - expressApp.use(express.json({ limit: "50mb" })); - register(expressApp, { - docs: { - v1: { - read: { - _root: getDocsReadService(app), - }, - write: { - _root: getDocsWriteService(app), - }, - }, - v2: { - read: { - _root: getDocsReadV2Service(app), - }, - write: { - _root: getDocsWriteV2Service(app), - }, - }, - }, - api: { - v1: { - read: { - _root: getReadApiService(app), - }, - register: { - _root: getRegisterApiService(app), - }, - }, - }, - snippets: getSnippetsService(app), - snippetsFactory: getSnippetsFactoryService(app), - templates: getTemplatesService(app), - diff: getApiDiffService(app), - docsCache: getDocsCacheService(app), - sdks: { - versions: getVersionsService(app), - }, - generators: { - _root: getGeneratorsRootController(app), - cli: getGeneratorsCliController(app), - versions: getGeneratorsVersionsController(app), - }, - tokens: getTokensService(app), - git: getGitController(app), - }); - registerBackgroundTasks(app); - app.logger.info(`Listening for requests on port ${PORT}`); - // The error handler must be registered before any other error middleware and after all controllers - expressApp.use(Sentry.Handlers.errorHandler()); - expressApp.listen(PORT); - } catch (err) { - app.logger.error("Failed to start express server", err); - } + try { + await app.initialize(); + expressApp.use(express.json({ limit: "50mb" })); + register(expressApp, { + docs: { + v1: { + read: { + _root: getDocsReadService(app), + }, + write: { + _root: getDocsWriteService(app), + }, + }, + v2: { + read: { + _root: getDocsReadV2Service(app), + }, + write: { + _root: getDocsWriteV2Service(app), + }, + }, + }, + api: { + v1: { + read: { + _root: getReadApiService(app), + }, + register: { + _root: getRegisterApiService(app), + }, + }, + }, + snippets: getSnippetsService(app), + snippetsFactory: getSnippetsFactoryService(app), + templates: getTemplatesService(app), + diff: getApiDiffService(app), + docsCache: getDocsCacheService(app), + sdks: { + versions: getVersionsService(app), + }, + generators: { + _root: getGeneratorsRootController(app), + cli: getGeneratorsCliController(app), + versions: getGeneratorsVersionsController(app), + }, + tokens: getTokensService(app), + git: getGitController(app), + }); + registerBackgroundTasks(app); + app.logger.info(`Listening for requests on port ${PORT}`); + // The error handler must be registered before any other error middleware and after all controllers + expressApp.use(Sentry.Handlers.errorHandler()); + expressApp.listen(PORT); + } catch (err) { + app.logger.error("Failed to start express server", err); + } } diff --git a/servers/fdr/src/services/algolia-index-segment-deleter/AlgoliaIndexSegmentDeleterService.ts b/servers/fdr/src/services/algolia-index-segment-deleter/AlgoliaIndexSegmentDeleterService.ts index 7bea462a2d..a430f67ed5 100644 --- a/servers/fdr/src/services/algolia-index-segment-deleter/AlgoliaIndexSegmentDeleterService.ts +++ b/servers/fdr/src/services/algolia-index-segment-deleter/AlgoliaIndexSegmentDeleterService.ts @@ -4,75 +4,94 @@ const MILLISECONDS_IN_ONE_MINUTE = 60 * 1_000; const MILLISECONDS_IN_ONE_HOUR = 60 * MILLISECONDS_IN_ONE_MINUTE; interface DeleteOldIndexSegmentsParams { - olderThanHours?: number; + olderThanHours?: number; } export interface AlgoliaIndexSegmentDeleterService { - deleteOldInactiveIndexSegments(params: DeleteOldIndexSegmentsParams): Promise; + deleteOldInactiveIndexSegments( + params: DeleteOldIndexSegmentsParams + ): Promise; } -export class AlgoliaIndexSegmentDeleterServiceImpl implements AlgoliaIndexSegmentDeleterService { - private static config = { - maxIndexSegmentsToQuery: 1_000, - }; +export class AlgoliaIndexSegmentDeleterServiceImpl + implements AlgoliaIndexSegmentDeleterService +{ + private static config = { + maxIndexSegmentsToQuery: 1_000, + }; - private get db() { - return this.app.services.db; - } - - constructor(private readonly app: FdrApplication) {} + private get db() { + return this.app.services.db; + } - public async deleteOldInactiveIndexSegments(params: DeleteOldIndexSegmentsParams) { - const { olderThanHours = 24 } = params; + constructor(private readonly app: FdrApplication) {} - const inactiveOldIndexSegmentIds = await this.db.prisma.$transaction(async (tx) => { - const activeIndexSegmentIds = await (async () => { - const docsRecords = await tx.docsV2.findMany({ - select: { indexSegmentIds: true }, - }); - return docsRecords - .map((r) => (Array.isArray(r.indexSegmentIds) ? r.indexSegmentIds : [])) - .reduce((acc, indexSegmentIds) => { - indexSegmentIds.forEach((indexSegmentId) => { - if (typeof indexSegmentId === "string") { - acc.add(indexSegmentId); - } - }); - return acc; - }, new Set()); - })(); + public async deleteOldInactiveIndexSegments( + params: DeleteOldIndexSegmentsParams + ) { + const { olderThanHours = 24 } = params; - const inactiveOldIndexSegmentIds = await (async () => { - const indexSegmentRecords = await tx.indexSegment.findMany({ - where: { - createdAt: { - lte: new Date(Date.now() - olderThanHours * MILLISECONDS_IN_ONE_HOUR), - }, - }, - select: { id: true }, - take: AlgoliaIndexSegmentDeleterServiceImpl.config.maxIndexSegmentsToQuery, - }); - return indexSegmentRecords.map((r) => r.id).filter((id) => !activeIndexSegmentIds.has(id)); - })(); + const inactiveOldIndexSegmentIds = await this.db.prisma.$transaction( + async (tx) => { + const activeIndexSegmentIds = await (async () => { + const docsRecords = await tx.docsV2.findMany({ + select: { indexSegmentIds: true }, + }); + return docsRecords + .map((r) => + Array.isArray(r.indexSegmentIds) ? r.indexSegmentIds : [] + ) + .reduce((acc, indexSegmentIds) => { + indexSegmentIds.forEach((indexSegmentId) => { + if (typeof indexSegmentId === "string") { + acc.add(indexSegmentId); + } + }); + return acc; + }, new Set()); + })(); - return inactiveOldIndexSegmentIds; - }); + const inactiveOldIndexSegmentIds = await (async () => { + const indexSegmentRecords = await tx.indexSegment.findMany({ + where: { + createdAt: { + lte: new Date( + Date.now() - olderThanHours * MILLISECONDS_IN_ONE_HOUR + ), + }, + }, + select: { id: true }, + take: AlgoliaIndexSegmentDeleterServiceImpl.config + .maxIndexSegmentsToQuery, + }); + return indexSegmentRecords + .map((r) => r.id) + .filter((id) => !activeIndexSegmentIds.has(id)); + })(); - for (const indexSegmentId of inactiveOldIndexSegmentIds) { - await this.deleteIndexSegmentAndNotifySlackIfFails(indexSegmentId); - } + return inactiveOldIndexSegmentIds; + } + ); - return inactiveOldIndexSegmentIds.length; + for (const indexSegmentId of inactiveOldIndexSegmentIds) { + await this.deleteIndexSegmentAndNotifySlackIfFails(indexSegmentId); } - private async deleteIndexSegmentAndNotifySlackIfFails(indexSegmentId: string) { - try { - await this.app.services.algolia.deleteIndexSegmentRecords([indexSegmentId]); - await this.db.prisma.indexSegment.delete({ - where: { id: indexSegmentId }, - }); - } catch (err) { - // await this.app.services.slack.notifyFailedToDeleteIndexSegment({ indexSegmentId, err }); - } + return inactiveOldIndexSegmentIds.length; + } + + private async deleteIndexSegmentAndNotifySlackIfFails( + indexSegmentId: string + ) { + try { + await this.app.services.algolia.deleteIndexSegmentRecords([ + indexSegmentId, + ]); + await this.db.prisma.indexSegment.delete({ + where: { id: indexSegmentId }, + }); + } catch (err) { + // await this.app.services.slack.notifyFailedToDeleteIndexSegment({ indexSegmentId, err }); } + } } diff --git a/servers/fdr/src/services/algolia-index-segment-deleter/index.ts b/servers/fdr/src/services/algolia-index-segment-deleter/index.ts index 4e59134896..c0ac55d75d 100644 --- a/servers/fdr/src/services/algolia-index-segment-deleter/index.ts +++ b/servers/fdr/src/services/algolia-index-segment-deleter/index.ts @@ -1,4 +1,4 @@ export { - AlgoliaIndexSegmentDeleterServiceImpl, - type AlgoliaIndexSegmentDeleterService, + AlgoliaIndexSegmentDeleterServiceImpl, + type AlgoliaIndexSegmentDeleterService, } from "./AlgoliaIndexSegmentDeleterService"; diff --git a/servers/fdr/src/services/algolia-index-segment-manager/AlgoliaIndexSegmentManagerService.ts b/servers/fdr/src/services/algolia-index-segment-manager/AlgoliaIndexSegmentManagerService.ts index af168032fa..b577dd24cc 100644 --- a/servers/fdr/src/services/algolia-index-segment-manager/AlgoliaIndexSegmentManagerService.ts +++ b/servers/fdr/src/services/algolia-index-segment-manager/AlgoliaIndexSegmentManagerService.ts @@ -1,4 +1,9 @@ -import { DocsV1Db, FdrAPI, FernNavigation, visitDbNavigationConfig } from "@fern-api/fdr-sdk"; +import { + DocsV1Db, + FdrAPI, + FernNavigation, + visitDbNavigationConfig, +} from "@fern-api/fdr-sdk"; import { addHours, addMinutes } from "date-fns"; import { kebabCase } from "es-toolkit/string"; import { v4 as uuidv4 } from "uuid"; @@ -10,202 +15,233 @@ import type { ConfigSegmentTuple, IndexSegment } from "../algolia"; const SECONDS_IN_ONE_HOUR = 60 * 60; type GenerateNewIndexSegmentsResult = - | { - type: "versioned"; - configSegmentTuples: ConfigSegmentTuple[]; - } - | { - type: "unversioned"; - configSegmentTuple: ConfigSegmentTuple; - }; + | { + type: "versioned"; + configSegmentTuples: ConfigSegmentTuple[]; + } + | { + type: "unversioned"; + configSegmentTuple: ConfigSegmentTuple; + }; export interface AlgoliaIndexSegmentManagerService { - getOrGenerateSearchApiKeyForIndexSegment(indexSegmentId: string): string; + getOrGenerateSearchApiKeyForIndexSegment(indexSegmentId: string): string; - getSearchApiKeyForIndexSegment(indexSegmentId: string): string | undefined; + getSearchApiKeyForIndexSegment(indexSegmentId: string): string | undefined; - generateAndCacheApiKey(indexSegmentId: string): string; + generateAndCacheApiKey(indexSegmentId: string): string; - generateIndexSegmentsForDefinition({ - dbDocsDefinition, - url, - }: { - dbDocsDefinition: DocsV1Db.DocsDefinitionDb; - url: string; - }): GenerateNewIndexSegmentsResult; + generateIndexSegmentsForDefinition({ + dbDocsDefinition, + url, + }: { + dbDocsDefinition: DocsV1Db.DocsDefinitionDb; + url: string; + }): GenerateNewIndexSegmentsResult; } -export class AlgoliaIndexSegmentManagerServiceImpl implements AlgoliaIndexSegmentManagerService { - private static config = { - apiKeyTTLHours: 8, - /** - * For example, if set to 30, API keys will stop functioning 30 minutes after they are removed from cache. - */ - apiKeyTTLExpiryDiffMinutes: 30, - maxKeysToCache: 5_000, - }; - - private apiKeysCache: Cache; - - private get algolia() { - return this.app.services.algolia; - } - - constructor(private readonly app: FdrApplication) { - this.apiKeysCache = new Cache(AlgoliaIndexSegmentManagerServiceImpl.config.maxKeysToCache, SECONDS_IN_ONE_HOUR); - } - - public generateIndexSegmentsForDefinition({ - dbDocsDefinition, - url, - }: { - dbDocsDefinition: DocsV1Db.DocsDefinitionDb; - url: string; - }): GenerateNewIndexSegmentsResult { - const navigationConfig = dbDocsDefinition.config.navigation; - - if (dbDocsDefinition.config.root != null) { - const latestRoot = FernNavigation.migrate.FernNavigationV1ToLatest.create().root( - dbDocsDefinition.config.root, - ); - - let versionedRootConfigSegmentTuples: ConfigSegmentTuple[] | undefined = undefined; - let unversionedRootConfigSegmentTuple: ConfigSegmentTuple | undefined = undefined; - - FernNavigation.traverseBF(latestRoot, (node) => { - if (node.type === "version") { - versionedRootConfigSegmentTuples ??= []; - versionedRootConfigSegmentTuples.push([ - undefined, - this.generateNewIndexSegmentForUnversionedNavigationConfig({ - url, - version: { - id: node.versionId, - urlSlug: node.slug.replace(new RegExp(`^${latestRoot.slug}/`), ""), - }, - }), - ]); - return true; - } else if (node.type === "unversioned") { - unversionedRootConfigSegmentTuple = [ - undefined, - this.generateNewIndexSegmentForUnversionedNavigationConfig({ - url, - }), - ]; - return false; - } - return true; - }); +export class AlgoliaIndexSegmentManagerServiceImpl + implements AlgoliaIndexSegmentManagerService +{ + private static config = { + apiKeyTTLHours: 8, + /** + * For example, if set to 30, API keys will stop functioning 30 minutes after they are removed from cache. + */ + apiKeyTTLExpiryDiffMinutes: 30, + maxKeysToCache: 5_000, + }; + + private apiKeysCache: Cache; + + private get algolia() { + return this.app.services.algolia; + } + + constructor(private readonly app: FdrApplication) { + this.apiKeysCache = new Cache( + AlgoliaIndexSegmentManagerServiceImpl.config.maxKeysToCache, + SECONDS_IN_ONE_HOUR + ); + } + + public generateIndexSegmentsForDefinition({ + dbDocsDefinition, + url, + }: { + dbDocsDefinition: DocsV1Db.DocsDefinitionDb; + url: string; + }): GenerateNewIndexSegmentsResult { + const navigationConfig = dbDocsDefinition.config.navigation; + + if (dbDocsDefinition.config.root != null) { + const latestRoot = + FernNavigation.migrate.FernNavigationV1ToLatest.create().root( + dbDocsDefinition.config.root + ); - if (versionedRootConfigSegmentTuples != null) { - return { type: "versioned", configSegmentTuples: versionedRootConfigSegmentTuples }; - } else if (unversionedRootConfigSegmentTuple != null) { - return { type: "unversioned", configSegmentTuple: unversionedRootConfigSegmentTuple }; - } - } else if (navigationConfig != null) { - return visitDbNavigationConfig(navigationConfig, { - versioned: (config) => { - const configSegmentTuples = config.versions.map((v) => { - const indexSegment = this.generateNewIndexSegmentForUnversionedNavigationConfig({ - url, - version: { id: v.version, urlSlug: v.urlSlug }, - }); - return [v.config, indexSegment] as const; - }); - return { - type: "versioned", - configSegmentTuples, - }; - }, - unversioned: (config) => { - const indexSegment = this.generateNewIndexSegmentForUnversionedNavigationConfig({ - url, - }); - return { - type: "unversioned", - configSegmentTuple: [config, indexSegment] as const, - }; - }, - }); + let versionedRootConfigSegmentTuples: ConfigSegmentTuple[] | undefined = + undefined; + let unversionedRootConfigSegmentTuple: ConfigSegmentTuple | undefined = + undefined; + + FernNavigation.traverseBF(latestRoot, (node) => { + if (node.type === "version") { + versionedRootConfigSegmentTuples ??= []; + versionedRootConfigSegmentTuples.push([ + undefined, + this.generateNewIndexSegmentForUnversionedNavigationConfig({ + url, + version: { + id: node.versionId, + urlSlug: node.slug.replace( + new RegExp(`^${latestRoot.slug}/`), + "" + ), + }, + }), + ]); + return true; + } else if (node.type === "unversioned") { + unversionedRootConfigSegmentTuple = [ + undefined, + this.generateNewIndexSegmentForUnversionedNavigationConfig({ + url, + }), + ]; + return false; } + return true; + }); + if (versionedRootConfigSegmentTuples != null) { return { - type: "versioned", - configSegmentTuples: [], + type: "versioned", + configSegmentTuples: versionedRootConfigSegmentTuples, }; - } - - private generateNewIndexSegmentForUnversionedNavigationConfig({ - version, - url, - }: { - version?: DocsVersion; - url: string; - }): IndexSegment { - const indexSegmentId = this.generateUniqueIdForIndexSegment({ url, version }); - const searchApiKey = this.generateAndCacheApiKey(indexSegmentId); - return version != null - ? { - type: "versioned", - id: indexSegmentId, - searchApiKey, - version, - } - : { - type: "unversioned", - id: indexSegmentId, - searchApiKey, - }; - } - - private generateUniqueIdForIndexSegment({ - version, - url, - }: { - version?: DocsVersion; - url: string; - }): FdrAPI.IndexSegmentId { - const parts: string[] = ["seg", url]; - if (version != null) { - parts.push(kebabCase(version.id)); + } else if (unversionedRootConfigSegmentTuple != null) { + return { + type: "unversioned", + configSegmentTuple: unversionedRootConfigSegmentTuple, + }; + } + } else if (navigationConfig != null) { + return visitDbNavigationConfig( + navigationConfig, + { + versioned: (config) => { + const configSegmentTuples = config.versions.map((v) => { + const indexSegment = + this.generateNewIndexSegmentForUnversionedNavigationConfig({ + url, + version: { id: v.version, urlSlug: v.urlSlug }, + }); + return [v.config, indexSegment] as const; + }); + return { + type: "versioned", + configSegmentTuples, + }; + }, + unversioned: (config) => { + const indexSegment = + this.generateNewIndexSegmentForUnversionedNavigationConfig({ + url, + }); + return { + type: "unversioned", + configSegmentTuple: [config, indexSegment] as const, + }; + }, } - parts.push(uuidv4()); - return FdrAPI.IndexSegmentId(parts.join("_")); + ); } - public getOrGenerateSearchApiKeyForIndexSegment(indexSegmentId: string) { - const cachedKey = this.apiKeysCache.get(indexSegmentId); - if (typeof cachedKey === "string") { - return cachedKey; - } else { - return this.generateAndCacheApiKey(indexSegmentId); + return { + type: "versioned", + configSegmentTuples: [], + }; + } + + private generateNewIndexSegmentForUnversionedNavigationConfig({ + version, + url, + }: { + version?: DocsVersion; + url: string; + }): IndexSegment { + const indexSegmentId = this.generateUniqueIdForIndexSegment({ + url, + version, + }); + const searchApiKey = this.generateAndCacheApiKey(indexSegmentId); + return version != null + ? { + type: "versioned", + id: indexSegmentId, + searchApiKey, + version, } + : { + type: "unversioned", + id: indexSegmentId, + searchApiKey, + }; + } + + private generateUniqueIdForIndexSegment({ + version, + url, + }: { + version?: DocsVersion; + url: string; + }): FdrAPI.IndexSegmentId { + const parts: string[] = ["seg", url]; + if (version != null) { + parts.push(kebabCase(version.id)); } - - public getSearchApiKeyForIndexSegment(indexSegmentId: string) { - const cachedKey = this.apiKeysCache.get(indexSegmentId); - return cachedKey; + parts.push(uuidv4()); + return FdrAPI.IndexSegmentId(parts.join("_")); + } + + public getOrGenerateSearchApiKeyForIndexSegment(indexSegmentId: string) { + const cachedKey = this.apiKeysCache.get(indexSegmentId); + if (typeof cachedKey === "string") { + return cachedKey; + } else { + return this.generateAndCacheApiKey(indexSegmentId); } - - public generateAndCacheApiKey(indexSegmentId: string) { - const now = new Date(); - const cacheUntil = addHours(now, AlgoliaIndexSegmentManagerServiceImpl.config.apiKeyTTLHours); - const validUntil = addMinutes( - cacheUntil, - AlgoliaIndexSegmentManagerServiceImpl.config.apiKeyTTLExpiryDiffMinutes, - ); - const key = this.algolia.generateSearchApiKey(`indexSegmentId:${indexSegmentId}`, validUntil); - this.tryCacheApiKey(indexSegmentId, key); - return key; - } - - private tryCacheApiKey(indexSegmentId: string, apiKey: string) { - try { - this.apiKeysCache.set(indexSegmentId, apiKey); - } catch { - // Cache is full, ignore error - return; - } + } + + public getSearchApiKeyForIndexSegment(indexSegmentId: string) { + const cachedKey = this.apiKeysCache.get(indexSegmentId); + return cachedKey; + } + + public generateAndCacheApiKey(indexSegmentId: string) { + const now = new Date(); + const cacheUntil = addHours( + now, + AlgoliaIndexSegmentManagerServiceImpl.config.apiKeyTTLHours + ); + const validUntil = addMinutes( + cacheUntil, + AlgoliaIndexSegmentManagerServiceImpl.config.apiKeyTTLExpiryDiffMinutes + ); + const key = this.algolia.generateSearchApiKey( + `indexSegmentId:${indexSegmentId}`, + validUntil + ); + this.tryCacheApiKey(indexSegmentId, key); + return key; + } + + private tryCacheApiKey(indexSegmentId: string, apiKey: string) { + try { + this.apiKeysCache.set(indexSegmentId, apiKey); + } catch { + // Cache is full, ignore error + return; } + } } diff --git a/servers/fdr/src/services/algolia-index-segment-manager/index.ts b/servers/fdr/src/services/algolia-index-segment-manager/index.ts index c2c2dcb586..37addfa6bb 100644 --- a/servers/fdr/src/services/algolia-index-segment-manager/index.ts +++ b/servers/fdr/src/services/algolia-index-segment-manager/index.ts @@ -1,4 +1,4 @@ export { - AlgoliaIndexSegmentManagerServiceImpl, - type AlgoliaIndexSegmentManagerService, + AlgoliaIndexSegmentManagerServiceImpl, + type AlgoliaIndexSegmentManagerService, } from "./AlgoliaIndexSegmentManagerService"; diff --git a/servers/fdr/src/services/algolia/AlgoliaSearchRecordGenerator.ts b/servers/fdr/src/services/algolia/AlgoliaSearchRecordGenerator.ts index 85fc8c07e4..53d7ed701d 100644 --- a/servers/fdr/src/services/algolia/AlgoliaSearchRecordGenerator.ts +++ b/servers/fdr/src/services/algolia/AlgoliaSearchRecordGenerator.ts @@ -1,916 +1,1011 @@ import { - APIV1Db, - APIV1Read, - Algolia, - DocsV1Db, - DocsV1Read, - FernNavigation, - convertDbAPIDefinitionToRead, - visitDbNavigationTab, - visitUnversionedDbNavigationConfig, + APIV1Db, + APIV1Read, + Algolia, + DocsV1Db, + DocsV1Read, + FernNavigation, + convertDbAPIDefinitionToRead, + visitDbNavigationTab, + visitUnversionedDbNavigationConfig, } from "@fern-api/fdr-sdk"; import { titleCase, visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; import { noop } from "es-toolkit/function"; import grayMatter from "gray-matter"; import { v4 as uuid } from "uuid"; import { LOGGER } from "../../app/FdrApplication"; -import { assertNever, convertMarkdownToText, truncateToBytes } from "../../util"; +import { + assertNever, + convertMarkdownToText, + truncateToBytes, +} from "../../util"; import { compact } from "../../util/object"; import { NavigationContext } from "./NavigationContext"; -import { ReferencedTypes, getAllReferencedTypes } from "./getAllReferencedTypes"; +import { + ReferencedTypes, + getAllReferencedTypes, +} from "./getAllReferencedTypes"; import type { AlgoliaSearchRecord, IndexSegment } from "./types"; interface AlgoliaSearchRecordGeneratorConfig { - docsDefinition: DocsV1Db.DocsDefinitionDb; - apiDefinitionsById: Record; + docsDefinition: DocsV1Db.DocsDefinitionDb; + apiDefinitionsById: Record; } export class AlgoliaSearchRecordGenerator { - public constructor(protected readonly config: AlgoliaSearchRecordGeneratorConfig) {} - - public generateAlgoliaSearchRecordsForSpecificDocsVersion( - navigationConfig: DocsV1Db.UnversionedNavigationConfig, - indexSegment: IndexSegment, - ): AlgoliaSearchRecord[] { - const context = new NavigationContext(indexSegment, []); - return this.generateAlgoliaSearchRecordsForUnversionedNavigationConfig(navigationConfig, context); + public constructor( + protected readonly config: AlgoliaSearchRecordGeneratorConfig + ) {} + + public generateAlgoliaSearchRecordsForSpecificDocsVersion( + navigationConfig: DocsV1Db.UnversionedNavigationConfig, + indexSegment: IndexSegment + ): AlgoliaSearchRecord[] { + const context = new NavigationContext(indexSegment, []); + return this.generateAlgoliaSearchRecordsForUnversionedNavigationConfig( + navigationConfig, + context + ); + } + + protected generateAlgoliaSearchRecordsForUnversionedNavigationConfig( + config: DocsV1Db.UnversionedNavigationConfig, + context: NavigationContext + ): AlgoliaSearchRecord[] { + return visitUnversionedDbNavigationConfig(config, { + tabbed: (tabbedConfig) => { + return this.generateAlgoliaSearchRecordsForUnversionedTabbedNavigationConfig( + tabbedConfig, + context + ); + }, + untabbed: (untabbedConfig) => { + return this.generateAlgoliaSearchRecordsForUnversionedUntabbedNavigationConfig( + untabbedConfig, + context + ); + }, + }); + } + + protected generateAlgoliaSearchRecordsForUnversionedUntabbedNavigationConfig( + config: DocsV1Db.UnversionedUntabbedNavigationConfig, + context: NavigationContext + ) { + const records = config.items.map((item) => + this.generateAlgoliaSearchRecordsForNavigationItem(item, context) + ); + return records.flat(1); + } + + protected generateAlgoliaSearchRecordsForUnversionedTabbedNavigationConfig( + config: DocsV1Db.UnversionedTabbedNavigationConfig, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const records = + config.tabsV2?.flatMap((tab) => { + switch (tab.type) { + case "group": + return tab.items.flatMap((item) => + this.generateAlgoliaSearchRecordsForNavigationItem( + item, + context.withPathPart({ + name: tab.title, + urlSlug: tab.urlSlug, + skipUrlSlug: tab.skipUrlSlug, + }) + ) + ); + case "changelog": + return this.generateAlgoliaSearchRecordsForChangelogSection( + tab, + context.withPathPart({ + name: tab.title ?? "Changelog", + urlSlug: tab.urlSlug, + skipUrlSlug: undefined, + }) + ); + default: + return []; + } + }) ?? + config.tabs?.map((tab) => + visitDbNavigationTab(tab, { + group: (group) => { + const tabRecords = group.items.map((item) => + this.generateAlgoliaSearchRecordsForNavigationItem( + item, + context.withPathPart({ + name: tab.title, + urlSlug: group.urlSlug, + skipUrlSlug: undefined, + }) + ) + ); + return tabRecords.flat(1); + }, + link: () => [], + }) + ) ?? + []; + return records.flat(1); + } + + protected generateAlgoliaSearchRecordsForNavigationItem( + item: DocsV1Db.NavigationItem, + context: NavigationContext + ): AlgoliaSearchRecord[] { + if (item.type === "section") { + if (item.hidden) { + return []; + } + const section = item; + const records = section.items.map((item) => + this.generateAlgoliaSearchRecordsForNavigationItem( + item, + context.withPathPart( + compact({ + name: section.title, + urlSlug: section.urlSlug, + skipUrlSlug: section.skipUrlSlug || undefined, + }) + ) + ) + ); + return records.flat(1); + } else if (item.type === "api") { + if (item.hidden) { + return []; + } + const records: AlgoliaSearchRecord[] = []; + const api = item; + const apiId = api.api; + const apiDef = this.config.apiDefinitionsById[apiId]; + if (apiDef != null) { + records.push( + ...this.generateAlgoliaSearchRecordsForApiDefinition( + apiDef, + context.withPathPart( + compact({ + name: api.title, + urlSlug: api.urlSlug, + skipUrlSlug: api.skipUrlSlug || undefined, + }) + ) + ) + ); + } + + if (item.changelog != null) { + records.push( + ...this.generateAlgoliaSearchRecordsForChangelogSection( + item.changelog, + context, + `${api.title} Changelog` + ) + ); + } + + return records; + } else if (item.type === "page") { + if (item.hidden) { + return []; + } + + const page = item; + const pageContent = this.config.docsDefinition.pages[page.id]; + if (pageContent == null) { + return []; + } + + const pageContext = + page.fullSlug != null + ? context.withFullSlug(page.fullSlug) + : context.withPathPart({ + name: page.title, + urlSlug: page.urlSlug, + skipUrlSlug: undefined, + }); + const processedContent = convertMarkdownToText(pageContent.markdown); + const { indexSegment } = context; + + return [ + compact({ + type: "page-v2", + objectID: uuid(), + title: page.title, // TODO: parse from frontmatter? + // TODO: Set to something more than 10kb on prod + // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ + content: truncateToBytes(processedContent, 50 * 1000), + path: { + parts: pageContext.pathParts, + }, + version: + indexSegment.type === "versioned" + ? { + id: indexSegment.version.id, + urlSlug: + indexSegment.version.urlSlug ?? indexSegment.version.id, + } + : undefined, + indexSegmentId: indexSegment.id, + }), + ]; + } else if (item.type === "link") { + return []; + } else if (item.type === "changelog") { + return this.generateAlgoliaSearchRecordsForChangelogSection( + item, + context + ); + } else if (item.type === "changelogV3") { + return this.generateAlgoliaSearchRecordsForChangelogNode( + item.node, + context + ); + } else if (item.type === "apiV2") { + return this.generateAlgoliaSearchRecordsForApiReferenceNode( + item.node, + context + ); } - - protected generateAlgoliaSearchRecordsForUnversionedNavigationConfig( - config: DocsV1Db.UnversionedNavigationConfig, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - return visitUnversionedDbNavigationConfig(config, { - tabbed: (tabbedConfig) => { - return this.generateAlgoliaSearchRecordsForUnversionedTabbedNavigationConfig(tabbedConfig, context); - }, - untabbed: (untabbedConfig) => { - return this.generateAlgoliaSearchRecordsForUnversionedUntabbedNavigationConfig(untabbedConfig, context); - }, - }); + assertNever(item); + } + + protected generateAlgoliaSearchRecordsForApiReferenceNode( + root: FernNavigation.V1.ApiReferenceNode, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const api = this.config.apiDefinitionsById[root.apiDefinitionId]; + if (api == null) { + LOGGER.error( + "Failed to find API definition for API reference node. id=", + root.apiDefinitionId + ); } - - protected generateAlgoliaSearchRecordsForUnversionedUntabbedNavigationConfig( - config: DocsV1Db.UnversionedUntabbedNavigationConfig, - context: NavigationContext, - ) { - const records = config.items.map((item) => this.generateAlgoliaSearchRecordsForNavigationItem(item, context)); - return records.flat(1); + const holder = + api != null + ? FernNavigation.ApiDefinitionHolder.create( + convertDbAPIDefinitionToRead(api) + ) + : undefined; + const records: AlgoliaSearchRecord[] = []; + + const breadcrumbs = context.pathParts.map((part) => part.name); + + const version = + context.indexSegment.type === "versioned" + ? ({ + id: context.indexSegment.version.id, + slug: FernNavigation.V1.Slug( + context.indexSegment.version.urlSlug ?? + context.indexSegment.version.id + ), + } satisfies Algolia.AlgoliaRecordVersionV3) + : undefined; + + function toBreadcrumbs( + parents: readonly FernNavigation.V1.NavigationNode[] + ): string[] { + return [ + ...breadcrumbs, + ...parents + .filter(FernNavigation.V1.hasMetadata) + .filter((parent) => + parent.type === "apiReference" + ? parent.hideTitle !== true + : parent.type === "changelogMonth" || + parent.type === "changelogYear" + ? false + : true + ) + .map((parent) => parent.title), + ]; } - protected generateAlgoliaSearchRecordsForUnversionedTabbedNavigationConfig( - config: DocsV1Db.UnversionedTabbedNavigationConfig, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const records = - config.tabsV2?.flatMap((tab) => { - switch (tab.type) { - case "group": - return tab.items.flatMap((item) => - this.generateAlgoliaSearchRecordsForNavigationItem( - item, - context.withPathPart({ - name: tab.title, - urlSlug: tab.urlSlug, - skipUrlSlug: tab.skipUrlSlug, - }), - ), - ); - case "changelog": - return this.generateAlgoliaSearchRecordsForChangelogSection( - tab, - context.withPathPart({ - name: tab.title ?? "Changelog", - urlSlug: tab.urlSlug, - skipUrlSlug: undefined, - }), - ); - default: - return []; - } - }) ?? - config.tabs?.map((tab) => - visitDbNavigationTab(tab, { - group: (group) => { - const tabRecords = group.items.map((item) => - this.generateAlgoliaSearchRecordsForNavigationItem( - item, - context.withPathPart({ - name: tab.title, - urlSlug: group.urlSlug, - skipUrlSlug: undefined, - }), - ), - ); - return tabRecords.flat(1); - }, - link: () => [], - }), - ) ?? - []; - return records.flat(1); - } - - protected generateAlgoliaSearchRecordsForNavigationItem( - item: DocsV1Db.NavigationItem, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - if (item.type === "section") { - if (item.hidden) { - return []; + FernNavigation.V1.traverseDF(root, (node, parents) => { + if (!FernNavigation.V1.hasMetadata(node)) { + return; + } + + if (node.hidden) { + return "skip"; + } + + if (FernNavigation.V1.isApiLeaf(node)) { + visitDiscriminatedUnion(node)._visit({ + endpoint: (node) => { + const endpoint = holder?.endpoints.get(node.endpointId); + if (endpoint == null) { + LOGGER.error( + "Failed to find endpoint for API reference node.", + node + ); + return; } - const section = item; - const records = section.items.map((item) => - this.generateAlgoliaSearchRecordsForNavigationItem( - item, - context.withPathPart( - compact({ - name: section.title, - urlSlug: section.urlSlug, - skipUrlSlug: section.skipUrlSlug || undefined, - }), - ), - ), - ); - return records.flat(1); - } else if (item.type === "api") { - if (item.hidden) { - return []; - } - const records: AlgoliaSearchRecord[] = []; - const api = item; - const apiId = api.api; - const apiDef = this.config.apiDefinitionsById[apiId]; - if (apiDef != null) { - records.push( - ...this.generateAlgoliaSearchRecordsForApiDefinition( - apiDef, - context.withPathPart( - compact({ - name: api.title, - urlSlug: api.urlSlug, - skipUrlSlug: api.skipUrlSlug || undefined, - }), - ), - ), + + // this is a hack to include the endpoint request/response json in the search index + // and potentially use it for conversational AI in the future. + // this needs to be rewritten as a template, with proper markdown formatting + snapshot testing. + // also, the content is potentially trimmed to 10kb. + const contents = [endpoint.description ?? ""]; + + const typeReferences: APIV1Read.TypeReference[] = []; + + if (endpoint.headers != null && endpoint.headers.length > 0) { + contents.push("## Headers\n"); + endpoint.headers.forEach((header) => { + typeReferences.push(header.type); + contents.push( + `- ${header.key}=${this.stringifyTypeRef(header.type)} ${header.description ?? ""}` ); + }); } - if (item.changelog != null) { - records.push( - ...this.generateAlgoliaSearchRecordsForChangelogSection( - item.changelog, - context, - `${api.title} Changelog`, - ), + if (endpoint.path.pathParameters.length > 0) { + contents.push("## Path Parameters\n"); + endpoint.path.pathParameters.forEach((param) => { + typeReferences.push(param.type); + contents.push( + `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}` ); + }); } - return records; - } else if (item.type === "page") { - if (item.hidden) { - return []; + if (endpoint.queryParameters.length > 0) { + contents.push("## Query Parameters\n"); + endpoint.queryParameters.forEach((param) => { + typeReferences.push(param.type); + contents.push( + `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}` + ); + }); } - const page = item; - const pageContent = this.config.docsDefinition.pages[page.id]; - if (pageContent == null) { - return []; + if (endpoint.request != null) { + contents.push("## Request\n"); + if (endpoint.request.description != null) { + contents.push(`${endpoint.request.description}\n`); + } + + contents.push("### Body\n"); + + if (endpoint.request.type.type === "reference") { + typeReferences.push(endpoint.request.type.value); + contents.push( + `${this.stringifyTypeRef(endpoint.request.type.value)}: ${endpoint.request.description ?? ""}` + ); + } else if (endpoint.request.type.type === "formData") { + endpoint.request.type.properties.forEach((property) => { + if (property.type === "bodyProperty") { + typeReferences.push(property.valueType); + contents.push( + `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}` + ); + } + }); + } else if (endpoint.request.type.type === "object") { + endpoint.request.type.extends.forEach((extend) => { + contents.push(`- ${extend}`); + }); + endpoint.request.type.properties.forEach((property) => { + typeReferences.push(property.valueType); + contents.push( + `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}` + ); + }); + } } - const pageContext = - page.fullSlug != null - ? context.withFullSlug(page.fullSlug) - : context.withPathPart({ - name: page.title, - urlSlug: page.urlSlug, - skipUrlSlug: undefined, - }); - const processedContent = convertMarkdownToText(pageContent.markdown); - const { indexSegment } = context; - - return [ - compact({ - type: "page-v2", - objectID: uuid(), - title: page.title, // TODO: parse from frontmatter? - // TODO: Set to something more than 10kb on prod - // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ - content: truncateToBytes(processedContent, 50 * 1000), - path: { - parts: pageContext.pathParts, - }, - version: - indexSegment.type === "versioned" - ? { - id: indexSegment.version.id, - urlSlug: indexSegment.version.urlSlug ?? indexSegment.version.id, - } - : undefined, - indexSegmentId: indexSegment.id, - }), - ]; - } else if (item.type === "link") { - return []; - } else if (item.type === "changelog") { - return this.generateAlgoliaSearchRecordsForChangelogSection(item, context); - } else if (item.type === "changelogV3") { - return this.generateAlgoliaSearchRecordsForChangelogNode(item.node, context); - } else if (item.type === "apiV2") { - return this.generateAlgoliaSearchRecordsForApiReferenceNode(item.node, context); - } - assertNever(item); - } + if (endpoint.response != null) { + contents.push("## Response\n"); + if (endpoint.response.description != null) { + contents.push(`${endpoint.response.description}\n`); + } - protected generateAlgoliaSearchRecordsForApiReferenceNode( - root: FernNavigation.V1.ApiReferenceNode, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const api = this.config.apiDefinitionsById[root.apiDefinitionId]; - if (api == null) { - LOGGER.error("Failed to find API definition for API reference node. id=", root.apiDefinitionId); - } - const holder = - api != null ? FernNavigation.ApiDefinitionHolder.create(convertDbAPIDefinitionToRead(api)) : undefined; - const records: AlgoliaSearchRecord[] = []; - - const breadcrumbs = context.pathParts.map((part) => part.name); - - const version = - context.indexSegment.type === "versioned" - ? ({ - id: context.indexSegment.version.id, - slug: FernNavigation.V1.Slug( - context.indexSegment.version.urlSlug ?? context.indexSegment.version.id, - ), - } satisfies Algolia.AlgoliaRecordVersionV3) - : undefined; - - function toBreadcrumbs(parents: readonly FernNavigation.V1.NavigationNode[]): string[] { - return [ - ...breadcrumbs, - ...parents - .filter(FernNavigation.V1.hasMetadata) - .filter((parent) => - parent.type === "apiReference" - ? parent.hideTitle !== true - : parent.type === "changelogMonth" || parent.type === "changelogYear" - ? false - : true, - ) - .map((parent) => parent.title), - ]; - } + contents.push("### Body\n"); - FernNavigation.V1.traverseDF(root, (node, parents) => { - if (!FernNavigation.V1.hasMetadata(node)) { - return; + if (endpoint.response.type.type === "reference") { + typeReferences.push(endpoint.response.type.value); + contents.push( + `${this.stringifyTypeRef(endpoint.response.type.value)}: ${endpoint.response.description ?? ""}` + ); + } else if (endpoint.response.type.type === "object") { + endpoint.response.type.extends.forEach((extend) => { + contents.push(`- ${extend}`); + }); + endpoint.response.type.properties.forEach((property) => { + typeReferences.push(property.valueType); + contents.push( + `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}` + ); + }); + } } - if (node.hidden) { - return "skip"; + contents.push( + this.collectReferencedTypesToContent( + typeReferences, + holder?.api.types ?? {} + ) + ); + + records.push( + compact({ + type: "endpoint-v3", + objectID: uuid(), + title: node.title, + content: truncateToBytes(contents.join("\n"), 50 * 1000), + breadcrumbs: toBreadcrumbs(parents), + slug: node.slug, + version, + indexSegmentId: context.indexSegment.id, + method: endpoint.method, + endpointPath: endpoint.path.parts, + isResponseStream: node.isResponseStream, + }) + ); + }, + webSocket: (node) => { + const ws = holder?.webSockets.get(node.webSocketId); + if (ws == null) { + LOGGER.error( + "Failed to find websocket for API reference node.", + node + ); + return; } - if (FernNavigation.V1.isApiLeaf(node)) { - visitDiscriminatedUnion(node)._visit({ - endpoint: (node) => { - const endpoint = holder?.endpoints.get(node.endpointId); - if (endpoint == null) { - LOGGER.error("Failed to find endpoint for API reference node.", node); - return; - } - - // this is a hack to include the endpoint request/response json in the search index - // and potentially use it for conversational AI in the future. - // this needs to be rewritten as a template, with proper markdown formatting + snapshot testing. - // also, the content is potentially trimmed to 10kb. - const contents = [endpoint.description ?? ""]; - - const typeReferences: APIV1Read.TypeReference[] = []; - - if (endpoint.headers != null && endpoint.headers.length > 0) { - contents.push("## Headers\n"); - endpoint.headers.forEach((header) => { - typeReferences.push(header.type); - contents.push( - `- ${header.key}=${this.stringifyTypeRef(header.type)} ${header.description ?? ""}`, - ); - }); - } - - if (endpoint.path.pathParameters.length > 0) { - contents.push("## Path Parameters\n"); - endpoint.path.pathParameters.forEach((param) => { - typeReferences.push(param.type); - contents.push( - `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}`, - ); - }); - } - - if (endpoint.queryParameters.length > 0) { - contents.push("## Query Parameters\n"); - endpoint.queryParameters.forEach((param) => { - typeReferences.push(param.type); - contents.push( - `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}`, - ); - }); - } - - if (endpoint.request != null) { - contents.push("## Request\n"); - if (endpoint.request.description != null) { - contents.push(`${endpoint.request.description}\n`); - } - - contents.push("### Body\n"); - - if (endpoint.request.type.type === "reference") { - typeReferences.push(endpoint.request.type.value); - contents.push( - `${this.stringifyTypeRef(endpoint.request.type.value)}: ${endpoint.request.description ?? ""}`, - ); - } else if (endpoint.request.type.type === "formData") { - endpoint.request.type.properties.forEach((property) => { - if (property.type === "bodyProperty") { - typeReferences.push(property.valueType); - contents.push( - `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}`, - ); - } - }); - } else if (endpoint.request.type.type === "object") { - endpoint.request.type.extends.forEach((extend) => { - contents.push(`- ${extend}`); - }); - endpoint.request.type.properties.forEach((property) => { - typeReferences.push(property.valueType); - contents.push( - `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}`, - ); - }); - } - } - - if (endpoint.response != null) { - contents.push("## Response\n"); - if (endpoint.response.description != null) { - contents.push(`${endpoint.response.description}\n`); - } - - contents.push("### Body\n"); - - if (endpoint.response.type.type === "reference") { - typeReferences.push(endpoint.response.type.value); - contents.push( - `${this.stringifyTypeRef(endpoint.response.type.value)}: ${endpoint.response.description ?? ""}`, - ); - } else if (endpoint.response.type.type === "object") { - endpoint.response.type.extends.forEach((extend) => { - contents.push(`- ${extend}`); - }); - endpoint.response.type.properties.forEach((property) => { - typeReferences.push(property.valueType); - contents.push( - `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}`, - ); - }); - } - } - - contents.push(this.collectReferencedTypesToContent(typeReferences, holder?.api.types ?? {})); - - records.push( - compact({ - type: "endpoint-v3", - objectID: uuid(), - title: node.title, - content: truncateToBytes(contents.join("\n"), 50 * 1000), - breadcrumbs: toBreadcrumbs(parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - method: endpoint.method, - endpointPath: endpoint.path.parts, - isResponseStream: node.isResponseStream, - }), - ); - }, - webSocket: (node) => { - const ws = holder?.webSockets.get(node.webSocketId); - if (ws == null) { - LOGGER.error("Failed to find websocket for API reference node.", node); - return; - } - - const contents = [ws.description ?? ""]; - - const typeReferences: APIV1Read.TypeReference[] = []; - - if (ws.headers.length > 0) { - contents.push("## Headers\n"); - ws.headers.forEach((param) => { - typeReferences.push(param.type); - contents.push( - `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}`, - ); - }); - } - - if (ws.path.pathParameters.length > 0) { - contents.push("## Path Parameters\n"); - ws.path.pathParameters.forEach((param) => { - typeReferences.push(param.type); - contents.push( - `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}`, - ); - }); - } - - if (ws.queryParameters.length > 0) { - contents.push("## Query Parameters\n"); - ws.queryParameters.forEach((param) => { - typeReferences.push(param.type); - contents.push( - `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}`, - ); - }); - } - - if (ws.messages.length > 0) { - contents.push("## Messages\n"); - ws.messages.forEach((message) => { - contents.push( - `### ${message.displayName ?? ""} (${message.type}) - ${message.origin}\n`, - ); - if (message.description != null) { - contents.push(message.description); - } - if (message.body.type === "reference") { - const anchorIdParts = [message.origin === "server" ? "receive" : "send"]; - if (message.displayName != null) { - anchorIdParts.push(message.displayName); - } - typeReferences.push(message.body.value); - contents.push(`- ${this.stringifyTypeRef(message.body.value)}`); - } else if (message.body.type === "object") { - message.body.extends.forEach((extend) => { - contents.push(`- ${extend}`); - }); - message.body.properties.forEach((property) => { - typeReferences.push(property.valueType); - contents.push( - `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}`, - ); - }); - } else { - assertNever(message.body); - } - }); - } - - contents.push(this.collectReferencedTypesToContent(typeReferences, holder?.api.types ?? {})); - - records.push( - compact({ - type: "websocket-v3", - objectID: uuid(), - title: node.title, - content: truncateToBytes(contents.join("\n"), 50 * 1000), - breadcrumbs: toBreadcrumbs(parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - endpointPath: ws.path.parts, - }), - ); - }, - webhook: (node) => { - const webhook = holder?.webhooks.get(node.webhookId); - if (webhook == null) { - LOGGER.error("Failed to find webhook for API reference node.", node); - return; - } - - const contents = [webhook.description ?? ""]; - const typeReferences: APIV1Read.TypeReference[] = []; - - if (webhook.headers.length > 0) { - contents.push("## Headers\n"); - webhook.headers.forEach((header) => { - typeReferences.push(header.type); - contents.push( - `- ${header.key}=${this.stringifyTypeRef(header.type)} ${header.description ?? ""}`, - ); - }); - } - - contents.push("## Payload\n"); - - if (webhook.payload.description != null) { - contents.push(webhook.payload.description); - } - - if (webhook.payload.type.type === "reference") { - typeReferences.push(webhook.payload.type.value); - contents.push( - `${this.stringifyTypeRef(webhook.payload.type.value)}: ${webhook.payload.description ?? ""}`, - ); - } else if (webhook.payload.type.type === "object") { - webhook.payload.type.extends.forEach((extend) => { - contents.push(`- ${extend}`); - }); - webhook.payload.type.properties.forEach((property) => { - typeReferences.push(property.valueType); - contents.push( - `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}`, - ); - }); - } else { - assertNever(webhook.payload.type); - } - - contents.push(this.collectReferencedTypesToContent(typeReferences, holder?.api.types ?? {})); - - records.push( - compact({ - type: "webhook-v3", - objectID: uuid(), - title: node.title, - content: truncateToBytes(contents.join("\n"), 50 * 1000), - breadcrumbs: toBreadcrumbs(parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - method: webhook.method, - endpointPath: webhook.path.map((path) => ({ type: "literal", value: path })), - }), - ); - }, - }); - } else if (FernNavigation.V1.hasMarkdown(node)) { - const pageId = FernNavigation.V1.getPageId(node); - if (pageId == null) { - return; - } + const contents = [ws.description ?? ""]; - const md = this.config.docsDefinition.pages[pageId]?.markdown; - if (md == null) { - LOGGER.error("Failed to find markdown for node", node); - return; - } + const typeReferences: APIV1Read.TypeReference[] = []; - const { frontmatter } = getFrontmatter(md); - - records.push( - compact({ - type: "page-v3", - objectID: uuid(), - title: frontmatter.title ?? node.title, - content: truncateToBytes(md, 50 * 1000), - breadcrumbs: toBreadcrumbs(parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - }), + if (ws.headers.length > 0) { + contents.push("## Headers\n"); + ws.headers.forEach((param) => { + typeReferences.push(param.type); + contents.push( + `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}` ); + }); } - return; - }); - return records; - } + if (ws.path.pathParameters.length > 0) { + contents.push("## Path Parameters\n"); + ws.path.pathParameters.forEach((param) => { + typeReferences.push(param.type); + contents.push( + `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}` + ); + }); + } - protected stringifyTypeRef(typeRef: APIV1Read.TypeReference): string { - return visitDiscriminatedUnion(typeRef)._visit({ - literal: (value) => value.value.value.toString(), - id: (value) => value.value, - primitive: (value) => value.value.type, - optional: (value) => `${this.stringifyTypeRef(value.itemType)}?`, - list: (value) => `${this.stringifyTypeRef(value.itemType)}[]`, - set: (value) => `Set<${this.stringifyTypeRef(value.itemType)}>`, - map: (value) => `Map<${this.stringifyTypeRef(value.keyType)}, ${this.stringifyTypeRef(value.valueType)}>`, - unknown: () => "unknown", - }); - } + if (ws.queryParameters.length > 0) { + contents.push("## Query Parameters\n"); + ws.queryParameters.forEach((param) => { + typeReferences.push(param.type); + contents.push( + `- ${param.key}=${this.stringifyTypeRef(param.type)} ${param.description ?? ""}` + ); + }); + } - protected collectReferencedTypesToContent( - typeReferences: APIV1Read.TypeReference[], - types: Record, - ): string { - let referencedTypes: ReferencedTypes = {}; + if (ws.messages.length > 0) { + contents.push("## Messages\n"); + ws.messages.forEach((message) => { + contents.push( + `### ${message.displayName ?? ""} (${message.type}) - ${message.origin}\n` + ); + if (message.description != null) { + contents.push(message.description); + } + if (message.body.type === "reference") { + const anchorIdParts = [ + message.origin === "server" ? "receive" : "send", + ]; + if (message.displayName != null) { + anchorIdParts.push(message.displayName); + } + typeReferences.push(message.body.value); + contents.push( + `- ${this.stringifyTypeRef(message.body.value)}` + ); + } else if (message.body.type === "object") { + message.body.extends.forEach((extend) => { + contents.push(`- ${extend}`); + }); + message.body.properties.forEach((property) => { + typeReferences.push(property.valueType); + contents.push( + `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}` + ); + }); + } else { + assertNever(message.body); + } + }); + } - typeReferences.forEach((typeReference) => { - const allReferencedTypes = getAllReferencedTypes({ - reference: typeReference, - types, - }); - referencedTypes = { - ...referencedTypes, - ...allReferencedTypes, - }; - }); + contents.push( + this.collectReferencedTypesToContent( + typeReferences, + holder?.api.types ?? {} + ) + ); - const contents = []; - if (Object.keys(referencedTypes).length > 0) { - contents.push("## Referenced Types\n"); - Object.entries(referencedTypes).forEach(([key, value]) => { - contents.push(`### ${key}\n`); + records.push( + compact({ + type: "websocket-v3", + objectID: uuid(), + title: node.title, + content: truncateToBytes(contents.join("\n"), 50 * 1000), + breadcrumbs: toBreadcrumbs(parents), + slug: node.slug, + version, + indexSegmentId: context.indexSegment.id, + endpointPath: ws.path.parts, + }) + ); + }, + webhook: (node) => { + const webhook = holder?.webhooks.get(node.webhookId); + if (webhook == null) { + LOGGER.error( + "Failed to find webhook for API reference node.", + node + ); + return; + } - if (value.description != null) { - contents.push(value.description); - } + const contents = [webhook.description ?? ""]; + const typeReferences: APIV1Read.TypeReference[] = []; - visitDiscriminatedUnion(value.shape)._visit({ - object: (object) => { - object.extends.forEach((extend) => { - contents.push(`- ${extend}`); - }); - object.properties.forEach((property) => { - contents.push( - `- ${property.key}: ${this.stringifyTypeRef(property.valueType)} - ${property.description ?? ""}`, - ); - }); - }, - alias: noop, - enum: (enum_) => { - enum_.values.forEach((value) => { - contents.push(`- ${value}`); - }); - }, - undiscriminatedUnion: (value) => { - value.variants.forEach((variant) => { - if (variant.displayName != null) { - contents.push(`#### ${variant.displayName}\n`); - } else if (variant.type.type === "id") { - contents.push(`#### ${variant.type.value}\n`); - } - contents.push( - `Type: ${this.stringifyTypeRef(variant.type)} - ${variant.description ?? ""}`, - ); - }); - }, - discriminatedUnion: (value) => { - value.variants.forEach((variant) => { - contents.push(`#### ${variant.displayName ?? titleCase(variant.discriminantValue)}\n`); - if (variant.description != null) { - contents.push(variant.description); - } - variant.additionalProperties.extends.forEach((extend) => { - contents.push(`- ${extend}`); - }); - variant.additionalProperties.properties.forEach((property) => { - contents.push( - `- ${property.key}: ${this.stringifyTypeRef(property.valueType)} - ${property.description ?? ""}`, - ); - }); - }); - }, - }); - }); - } + if (webhook.headers.length > 0) { + contents.push("## Headers\n"); + webhook.headers.forEach((header) => { + typeReferences.push(header.type); + contents.push( + `- ${header.key}=${this.stringifyTypeRef(header.type)} ${header.description ?? ""}` + ); + }); + } - return contents.join("\n"); - } + contents.push("## Payload\n"); - protected generateAlgoliaSearchRecordsForChangelogNode( - root: FernNavigation.V1.ChangelogNode, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const records: AlgoliaSearchRecord[] = []; + if (webhook.payload.description != null) { + contents.push(webhook.payload.description); + } - const breadcrumbs = context.pathParts.map((part) => part.name); + if (webhook.payload.type.type === "reference") { + typeReferences.push(webhook.payload.type.value); + contents.push( + `${this.stringifyTypeRef(webhook.payload.type.value)}: ${webhook.payload.description ?? ""}` + ); + } else if (webhook.payload.type.type === "object") { + webhook.payload.type.extends.forEach((extend) => { + contents.push(`- ${extend}`); + }); + webhook.payload.type.properties.forEach((property) => { + typeReferences.push(property.valueType); + contents.push( + `- ${property.key}=${this.stringifyTypeRef(property.valueType)} ${property.description ?? ""}` + ); + }); + } else { + assertNever(webhook.payload.type); + } - const version = - context.indexSegment.type === "versioned" - ? { - id: context.indexSegment.version.id, - slug: FernNavigation.V1.Slug( - context.indexSegment.version.urlSlug ?? context.indexSegment.version.id, - ), - } - : undefined; - - function toBreadcrumbs(parents: readonly FernNavigation.V1.NavigationNode[]): string[] { - return [ - ...breadcrumbs, - ...parents - .filter(FernNavigation.V1.hasMetadata) - .filter((parent) => - parent.type === "apiReference" - ? parent.hideTitle !== true - : parent.type === "changelogMonth" || parent.type === "changelogYear" - ? false - : true, - ) - .map((parent) => parent.title), - ]; + contents.push( + this.collectReferencedTypesToContent( + typeReferences, + holder?.api.types ?? {} + ) + ); + + records.push( + compact({ + type: "webhook-v3", + objectID: uuid(), + title: node.title, + content: truncateToBytes(contents.join("\n"), 50 * 1000), + breadcrumbs: toBreadcrumbs(parents), + slug: node.slug, + version, + indexSegmentId: context.indexSegment.id, + method: webhook.method, + endpointPath: webhook.path.map((path) => ({ + type: "literal", + value: path, + })), + }) + ); + }, + }); + } else if (FernNavigation.V1.hasMarkdown(node)) { + const pageId = FernNavigation.V1.getPageId(node); + if (pageId == null) { + return; } - FernNavigation.V1.traverseDF(root, (node, parents) => { - if (!FernNavigation.V1.hasMetadata(node)) { - return; - } + const md = this.config.docsDefinition.pages[pageId]?.markdown; + if (md == null) { + LOGGER.error("Failed to find markdown for node", node); + return; + } - if (node.hidden) { - return "skip"; - } + const { frontmatter } = getFrontmatter(md); + + records.push( + compact({ + type: "page-v3", + objectID: uuid(), + title: frontmatter.title ?? node.title, + content: truncateToBytes(md, 50 * 1000), + breadcrumbs: toBreadcrumbs(parents), + slug: node.slug, + version, + indexSegmentId: context.indexSegment.id, + }) + ); + } + return; + }); - if (FernNavigation.V1.hasMarkdown(node)) { - const pageId = FernNavigation.V1.getPageId(node); - if (pageId == null) { - return; - } + return records; + } + + protected stringifyTypeRef(typeRef: APIV1Read.TypeReference): string { + return visitDiscriminatedUnion(typeRef)._visit({ + literal: (value) => value.value.value.toString(), + id: (value) => value.value, + primitive: (value) => value.value.type, + optional: (value) => `${this.stringifyTypeRef(value.itemType)}?`, + list: (value) => `${this.stringifyTypeRef(value.itemType)}[]`, + set: (value) => `Set<${this.stringifyTypeRef(value.itemType)}>`, + map: (value) => + `Map<${this.stringifyTypeRef(value.keyType)}, ${this.stringifyTypeRef(value.valueType)}>`, + unknown: () => "unknown", + }); + } + + protected collectReferencedTypesToContent( + typeReferences: APIV1Read.TypeReference[], + types: Record + ): string { + let referencedTypes: ReferencedTypes = {}; + + typeReferences.forEach((typeReference) => { + const allReferencedTypes = getAllReferencedTypes({ + reference: typeReference, + types, + }); + referencedTypes = { + ...referencedTypes, + ...allReferencedTypes, + }; + }); - const md = this.config.docsDefinition.pages[pageId]?.markdown; - if (md == null) { - LOGGER.error("Failed to find markdown for node", node); - return; - } + const contents = []; + if (Object.keys(referencedTypes).length > 0) { + contents.push("## Referenced Types\n"); + Object.entries(referencedTypes).forEach(([key, value]) => { + contents.push(`### ${key}\n`); + + if (value.description != null) { + contents.push(value.description); + } - records.push( - compact({ - type: "page-v3", - objectID: uuid(), - title: node.title, - content: truncateToBytes(md, 50 * 1000), - breadcrumbs: toBreadcrumbs(parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - }), + visitDiscriminatedUnion(value.shape)._visit({ + object: (object) => { + object.extends.forEach((extend) => { + contents.push(`- ${extend}`); + }); + object.properties.forEach((property) => { + contents.push( + `- ${property.key}: ${this.stringifyTypeRef(property.valueType)} - ${property.description ?? ""}` + ); + }); + }, + alias: noop, + enum: (enum_) => { + enum_.values.forEach((value) => { + contents.push(`- ${value}`); + }); + }, + undiscriminatedUnion: (value) => { + value.variants.forEach((variant) => { + if (variant.displayName != null) { + contents.push(`#### ${variant.displayName}\n`); + } else if (variant.type.type === "id") { + contents.push(`#### ${variant.type.value}\n`); + } + contents.push( + `Type: ${this.stringifyTypeRef(variant.type)} - ${variant.description ?? ""}` + ); + }); + }, + discriminatedUnion: (value) => { + value.variants.forEach((variant) => { + contents.push( + `#### ${variant.displayName ?? titleCase(variant.discriminantValue)}\n` + ); + if (variant.description != null) { + contents.push(variant.description); + } + variant.additionalProperties.extends.forEach((extend) => { + contents.push(`- ${extend}`); + }); + variant.additionalProperties.properties.forEach((property) => { + contents.push( + `- ${property.key}: ${this.stringifyTypeRef(property.valueType)} - ${property.description ?? ""}` ); - } - return; + }); + }); + }, }); + }); + } - return records; + return contents.join("\n"); + } + + protected generateAlgoliaSearchRecordsForChangelogNode( + root: FernNavigation.V1.ChangelogNode, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const records: AlgoliaSearchRecord[] = []; + + const breadcrumbs = context.pathParts.map((part) => part.name); + + const version = + context.indexSegment.type === "versioned" + ? { + id: context.indexSegment.version.id, + slug: FernNavigation.V1.Slug( + context.indexSegment.version.urlSlug ?? + context.indexSegment.version.id + ), + } + : undefined; + + function toBreadcrumbs( + parents: readonly FernNavigation.V1.NavigationNode[] + ): string[] { + return [ + ...breadcrumbs, + ...parents + .filter(FernNavigation.V1.hasMetadata) + .filter((parent) => + parent.type === "apiReference" + ? parent.hideTitle !== true + : parent.type === "changelogMonth" || + parent.type === "changelogYear" + ? false + : true + ) + .map((parent) => parent.title), + ]; } - protected generateAlgoliaSearchRecordsForChangelogSection( - changelog: DocsV1Read.ChangelogSection, - context: NavigationContext, - fallbackTitle: string = "Changelog", - ): AlgoliaSearchRecord[] { - if (changelog.hidden) { - return []; - } - const records: AlgoliaSearchRecord[] = []; - if (changelog.pageId != null) { - const changelogPageContent = this.config.docsDefinition.pages[changelog.pageId]; - const urlSlug = changelog.urlSlug; - const title = changelog.title ?? fallbackTitle; - - if (changelogPageContent != null) { - const processedContent = convertMarkdownToText(changelogPageContent.markdown); - const { indexSegment } = context; - const pageContext = context.withPathPart({ - // TODO: parse from frontmatter? - name: title, - urlSlug, - skipUrlSlug: undefined, - }); - records.push( - compact({ - type: "page-v2", - objectID: uuid(), - title, - // TODO: Set to something more than 10kb on prod - // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ - content: truncateToBytes(processedContent, 50 * 1000), - path: { - parts: pageContext.pathParts, - }, - version: - indexSegment.type === "versioned" - ? { - id: indexSegment.version.id, - urlSlug: indexSegment.version.urlSlug ?? indexSegment.version.id, - } - : undefined, - indexSegmentId: indexSegment.id, - }), - ); - } + FernNavigation.V1.traverseDF(root, (node, parents) => { + if (!FernNavigation.V1.hasMetadata(node)) { + return; + } - changelog.items.forEach((changelogItem) => { - const changelogItemContext = context.withPathPart({ - name: `${title} - ${changelogItem.date}`, - urlSlug, - skipUrlSlug: undefined, - }); + if (node.hidden) { + return "skip"; + } - const changelogPageContent = this.config.docsDefinition.pages[changelogItem.pageId]; - if (changelogPageContent != null) { - const processedContent = convertMarkdownToText(changelogPageContent.markdown); - const { indexSegment } = context; - - records.push( - compact({ - type: "page-v2", - objectID: uuid(), - title: `${title} - ${changelogItem.date}`, - // TODO: Set to something more than 10kb on prod - // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ - content: truncateToBytes(processedContent, 50 * 1000), - path: { - parts: changelogItemContext.pathParts, - // TODO: add anchor - }, - version: - indexSegment.type === "versioned" - ? { - id: indexSegment.version.id, - urlSlug: indexSegment.version.urlSlug ?? indexSegment.version.id, - } - : undefined, - indexSegmentId: indexSegment.id, - }), - ); - } - }); + if (FernNavigation.V1.hasMarkdown(node)) { + const pageId = FernNavigation.V1.getPageId(node); + if (pageId == null) { + return; } - return records; - } + const md = this.config.docsDefinition.pages[pageId]?.markdown; + if (md == null) { + LOGGER.error("Failed to find markdown for node", node); + return; + } - protected generateAlgoliaSearchRecordsForApiDefinition( - apiDef: APIV1Db.DbApiDefinition, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const { rootPackage, subpackages } = apiDef; - const subpackagePathParts = getPathPartsBySubpackage({ definition: apiDef }); + records.push( + compact({ + type: "page-v3", + objectID: uuid(), + title: node.title, + content: truncateToBytes(md, 50 * 1000), + breadcrumbs: toBreadcrumbs(parents), + slug: node.slug, + version, + indexSegmentId: context.indexSegment.id, + }) + ); + } + return; + }); - const records: AlgoliaSearchRecord[] = []; + return records; + } - rootPackage.endpoints.forEach((e) => { - const endpointRecords = this.generateAlgoliaSearchRecordsForEndpointDefinition(e, context); - records.push(...endpointRecords); + protected generateAlgoliaSearchRecordsForChangelogSection( + changelog: DocsV1Read.ChangelogSection, + context: NavigationContext, + fallbackTitle: string = "Changelog" + ): AlgoliaSearchRecord[] { + if (changelog.hidden) { + return []; + } + const records: AlgoliaSearchRecord[] = []; + if (changelog.pageId != null) { + const changelogPageContent = + this.config.docsDefinition.pages[changelog.pageId]; + const urlSlug = changelog.urlSlug; + const title = changelog.title ?? fallbackTitle; + + if (changelogPageContent != null) { + const processedContent = convertMarkdownToText( + changelogPageContent.markdown + ); + const { indexSegment } = context; + const pageContext = context.withPathPart({ + // TODO: parse from frontmatter? + name: title, + urlSlug, + skipUrlSlug: undefined, }); - - Object.entries(subpackages).forEach(([id, subpackage]) => { - const pathParts = subpackagePathParts[APIV1Db.SubpackageId(id)]; - if (pathParts == null) { - LOGGER.error("Excluding subpackage from search. Failed to find path parts for subpackage id=", id); - return; - } - subpackage.endpoints.forEach((e) => { - const endpointRecords = this.generateAlgoliaSearchRecordsForEndpointDefinition( - e, - context.withPathParts(pathParts), - ); - records.push(...endpointRecords); - }); + records.push( + compact({ + type: "page-v2", + objectID: uuid(), + title, + // TODO: Set to something more than 10kb on prod + // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ + content: truncateToBytes(processedContent, 50 * 1000), + path: { + parts: pageContext.pathParts, + }, + version: + indexSegment.type === "versioned" + ? { + id: indexSegment.version.id, + urlSlug: + indexSegment.version.urlSlug ?? indexSegment.version.id, + } + : undefined, + indexSegmentId: indexSegment.id, + }) + ); + } + + changelog.items.forEach((changelogItem) => { + const changelogItemContext = context.withPathPart({ + name: `${title} - ${changelogItem.date}`, + urlSlug, + skipUrlSlug: undefined, }); - return records; + const changelogPageContent = + this.config.docsDefinition.pages[changelogItem.pageId]; + if (changelogPageContent != null) { + const processedContent = convertMarkdownToText( + changelogPageContent.markdown + ); + const { indexSegment } = context; + + records.push( + compact({ + type: "page-v2", + objectID: uuid(), + title: `${title} - ${changelogItem.date}`, + // TODO: Set to something more than 10kb on prod + // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ + content: truncateToBytes(processedContent, 50 * 1000), + path: { + parts: changelogItemContext.pathParts, + // TODO: add anchor + }, + version: + indexSegment.type === "versioned" + ? { + id: indexSegment.version.id, + urlSlug: + indexSegment.version.urlSlug ?? indexSegment.version.id, + } + : undefined, + indexSegmentId: indexSegment.id, + }) + ); + } + }); } - protected generateAlgoliaSearchRecordsForEndpointDefinition( - endpointDef: APIV1Db.DbEndpointDefinition, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const records: AlgoliaSearchRecord[] = []; - if (endpointDef.name != null || endpointDef.description != null) { - const endpointContext = context.withPathPart({ - name: endpointDef.name ?? "", - urlSlug: endpointDef.urlSlug, - skipUrlSlug: undefined, - }); - const { indexSegment } = context; - records.push( - compact({ - type: "endpoint-v2", - objectID: uuid(), - endpoint: { - name: endpointDef.name, - description: - endpointDef.description != null - ? convertMarkdownToText(endpointDef.description) - : undefined, - method: endpointDef.method, - path: { - parts: endpointDef.path.parts, - }, - }, - path: { - parts: endpointContext.pathParts, - }, - version: - indexSegment.type === "versioned" - ? { - id: indexSegment.version.id, - urlSlug: indexSegment.version.urlSlug ?? indexSegment.version.id, - } - : undefined, - indexSegmentId: indexSegment.id, - }), - ); - } - // Add records for query parameters, request/response body etc. - return records; + return records; + } + + protected generateAlgoliaSearchRecordsForApiDefinition( + apiDef: APIV1Db.DbApiDefinition, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const { rootPackage, subpackages } = apiDef; + const subpackagePathParts = getPathPartsBySubpackage({ + definition: apiDef, + }); + + const records: AlgoliaSearchRecord[] = []; + + rootPackage.endpoints.forEach((e) => { + const endpointRecords = + this.generateAlgoliaSearchRecordsForEndpointDefinition(e, context); + records.push(...endpointRecords); + }); + + Object.entries(subpackages).forEach(([id, subpackage]) => { + const pathParts = subpackagePathParts[APIV1Db.SubpackageId(id)]; + if (pathParts == null) { + LOGGER.error( + "Excluding subpackage from search. Failed to find path parts for subpackage id=", + id + ); + return; + } + subpackage.endpoints.forEach((e) => { + const endpointRecords = + this.generateAlgoliaSearchRecordsForEndpointDefinition( + e, + context.withPathParts(pathParts) + ); + records.push(...endpointRecords); + }); + }); + + return records; + } + + protected generateAlgoliaSearchRecordsForEndpointDefinition( + endpointDef: APIV1Db.DbEndpointDefinition, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const records: AlgoliaSearchRecord[] = []; + if (endpointDef.name != null || endpointDef.description != null) { + const endpointContext = context.withPathPart({ + name: endpointDef.name ?? "", + urlSlug: endpointDef.urlSlug, + skipUrlSlug: undefined, + }); + const { indexSegment } = context; + records.push( + compact({ + type: "endpoint-v2", + objectID: uuid(), + endpoint: { + name: endpointDef.name, + description: + endpointDef.description != null + ? convertMarkdownToText(endpointDef.description) + : undefined, + method: endpointDef.method, + path: { + parts: endpointDef.path.parts, + }, + }, + path: { + parts: endpointContext.pathParts, + }, + version: + indexSegment.type === "versioned" + ? { + id: indexSegment.version.id, + urlSlug: + indexSegment.version.urlSlug ?? indexSegment.version.id, + } + : undefined, + indexSegmentId: indexSegment.id, + }) + ); } + // Add records for query parameters, request/response body etc. + return records; + } } // interface PathPart { @@ -920,102 +1015,112 @@ export class AlgoliaSearchRecordGenerator { // } function getPathPartsBySubpackage({ - definition, + definition, }: { - definition: APIV1Db.DbApiDefinition; + definition: APIV1Db.DbApiDefinition; }): Record { - return getPathPartsBySubpackageHelper({ - definition, - subpackages: getSubpackagesMap({ definition, subpackages: definition.rootPackage.subpackages }), - pathParts: [], - }); + return getPathPartsBySubpackageHelper({ + definition, + subpackages: getSubpackagesMap({ + definition, + subpackages: definition.rootPackage.subpackages, + }), + pathParts: [], + }); } function getPathPartsBySubpackageHelper({ - definition, - subpackages, - pathParts, + definition, + subpackages, + pathParts, }: { - definition: APIV1Db.DbApiDefinition; - subpackages: Record; - pathParts: Algolia.AlgoliaRecordPathPart[]; + definition: APIV1Db.DbApiDefinition; + subpackages: Record< + APIV1Read.SubpackageId, + APIV1Db.DbApiDefinitionSubpackage + >; + pathParts: Algolia.AlgoliaRecordPathPart[]; }): Record { - let result: Record = {}; - for (const [id, subpackage] of Object.entries(subpackages)) { - if (subpackage.pointsTo != null) { - const pointedToSubpackage = definition.subpackages[subpackage.pointsTo]; - if (pointedToSubpackage == null) { - LOGGER.error("Failed to find pointedTo subpackage for API. id=", id); - continue; - } - result = { - ...result, - ...getPathPartsBySubpackageHelper({ - definition, - subpackages: { - [subpackage.pointsTo]: { - ...pointedToSubpackage, - urlSlug: subpackage.urlSlug, - name: subpackage.name, - }, - }, - pathParts, - }), - }; - } else { - const path: Algolia.AlgoliaRecordPathPart[] = [ - ...pathParts, - { - name: subpackage.name, - urlSlug: subpackage.urlSlug, - skipUrlSlug: undefined, - }, - ]; - result[APIV1Db.SubpackageId(id)] = path; - result = { - ...result, - ...getPathPartsBySubpackageHelper({ - definition, - subpackages: getSubpackagesMap({ definition, subpackages: subpackage.subpackages }), - pathParts: path, - }), - }; - } + let result: Record = + {}; + for (const [id, subpackage] of Object.entries(subpackages)) { + if (subpackage.pointsTo != null) { + const pointedToSubpackage = definition.subpackages[subpackage.pointsTo]; + if (pointedToSubpackage == null) { + LOGGER.error("Failed to find pointedTo subpackage for API. id=", id); + continue; + } + result = { + ...result, + ...getPathPartsBySubpackageHelper({ + definition, + subpackages: { + [subpackage.pointsTo]: { + ...pointedToSubpackage, + urlSlug: subpackage.urlSlug, + name: subpackage.name, + }, + }, + pathParts, + }), + }; + } else { + const path: Algolia.AlgoliaRecordPathPart[] = [ + ...pathParts, + { + name: subpackage.name, + urlSlug: subpackage.urlSlug, + skipUrlSlug: undefined, + }, + ]; + result[APIV1Db.SubpackageId(id)] = path; + result = { + ...result, + ...getPathPartsBySubpackageHelper({ + definition, + subpackages: getSubpackagesMap({ + definition, + subpackages: subpackage.subpackages, + }), + pathParts: path, + }), + }; } - return result; + } + return result; } function getSubpackagesMap({ - definition, - subpackages, + definition, + subpackages, }: { - definition: APIV1Db.DbApiDefinition; - subpackages: APIV1Read.SubpackageId[]; + definition: APIV1Db.DbApiDefinition; + subpackages: APIV1Read.SubpackageId[]; }): Record { - return Object.fromEntries( - subpackages.map((id) => { - const subpackage = definition.subpackages[id]; - if (subpackage == null) { - LOGGER.error("Failed to find subpackage for API. id=", id); - return []; - } - return [id, subpackage]; - }), - ); + return Object.fromEntries( + subpackages.map((id) => { + const subpackage = definition.subpackages[id]; + if (subpackage == null) { + LOGGER.error("Failed to find subpackage for API. id=", id); + return []; + } + return [id, subpackage]; + }) + ); } interface Frontmatter { - title?: string; // overrides sidebar title + title?: string; // overrides sidebar title } export function getFrontmatter(content: string): { - frontmatter: Frontmatter; - content: string; + frontmatter: Frontmatter; + content: string; } { - try { - const gm = grayMatter(content); - return { frontmatter: gm.data, content: gm.content }; - } catch (e) { - return { frontmatter: {}, content }; - } + try { + const gm = grayMatter(content); + return { frontmatter: gm.data, content: gm.content }; + } catch (e) { + return { frontmatter: {}, content }; + } } diff --git a/servers/fdr/src/services/algolia/AlgoliaSearchRecordGeneratorV2.ts b/servers/fdr/src/services/algolia/AlgoliaSearchRecordGeneratorV2.ts index 4afea56b66..bd2ca90b88 100644 --- a/servers/fdr/src/services/algolia/AlgoliaSearchRecordGeneratorV2.ts +++ b/servers/fdr/src/services/algolia/AlgoliaSearchRecordGeneratorV2.ts @@ -1,12 +1,12 @@ import { - APIV1Read, - Algolia, - DocsV1Db, - DocsV1Read, - FdrAPI, - FernNavigation, - convertDbAPIDefinitionToRead, - visitDbNavigationTab, + APIV1Read, + Algolia, + DocsV1Db, + DocsV1Read, + FdrAPI, + FernNavigation, + convertDbAPIDefinitionToRead, + visitDbNavigationTab, } from "@fern-api/fdr-sdk"; import { titleCase, visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; import { kebabCase } from "es-toolkit/string"; @@ -14,1737 +14,1866 @@ import { v4 as uuid } from "uuid"; import { EndpointPathPart } from "../../../../../packages/fdr-sdk/src/client/APIV1Read"; import { BreadcrumbsInfo } from "../../api/generated/api"; import { LOGGER } from "../../app/FdrApplication"; -import { assertNever, convertMarkdownToText, truncateToBytes } from "../../util"; +import { + assertNever, + convertMarkdownToText, + truncateToBytes, +} from "../../util"; import { compact } from "../../util/object"; -import { AlgoliaSearchRecordGenerator, getFrontmatter } from "./AlgoliaSearchRecordGenerator"; +import { + AlgoliaSearchRecordGenerator, + getFrontmatter, +} from "./AlgoliaSearchRecordGenerator"; import { NavigationContext } from "./NavigationContext"; -import type { AlgoliaSearchRecord, IndexSegment, MarkdownNode, TypeReferenceWithMetadata } from "./types"; +import type { + AlgoliaSearchRecord, + IndexSegment, + MarkdownNode, + TypeReferenceWithMetadata, +} from "./types"; export class AlgoliaSearchRecordGeneratorV2 extends AlgoliaSearchRecordGenerator { - protected generateAlgoliaSearchRecordsForSectionNavigationItem( - item: DocsV1Db.NavigationItem.Section, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - if (item.hidden) { - return []; - } - const section = item; - const records = section.items.map((item) => - this.generateAlgoliaSearchRecordsForNavigationItem( - item, - context.withPathPart( - compact({ - name: section.title, - urlSlug: section.urlSlug, - skipUrlSlug: section.skipUrlSlug || undefined, - }), - ), - ), - ); - return records.flat(1); + protected generateAlgoliaSearchRecordsForSectionNavigationItem( + item: DocsV1Db.NavigationItem.Section, + context: NavigationContext + ): AlgoliaSearchRecord[] { + if (item.hidden) { + return []; + } + const section = item; + const records = section.items.map((item) => + this.generateAlgoliaSearchRecordsForNavigationItem( + item, + context.withPathPart( + compact({ + name: section.title, + urlSlug: section.urlSlug, + skipUrlSlug: section.skipUrlSlug || undefined, + }) + ) + ) + ); + return records.flat(1); + } + + protected generateAlgoliaSectionRecordsForApiNavigationItem( + item: DocsV1Db.NavigationItem.Api, + context: NavigationContext + ): AlgoliaSearchRecord[] { + if (item.hidden) { + return []; + } + const records: AlgoliaSearchRecord[] = []; + const api = item; + const apiId = api.api; + const apiDef = this.config.apiDefinitionsById[apiId]; + if (apiDef != null) { + records.push( + ...this.generateAlgoliaSearchRecordsForApiDefinition( + apiDef, + context.withPathPart( + compact({ + name: api.title, + urlSlug: api.urlSlug, + skipUrlSlug: api.skipUrlSlug || undefined, + }) + ) + ) + ); } - protected generateAlgoliaSectionRecordsForApiNavigationItem( - item: DocsV1Db.NavigationItem.Api, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - if (item.hidden) { - return []; - } - const records: AlgoliaSearchRecord[] = []; - const api = item; - const apiId = api.api; - const apiDef = this.config.apiDefinitionsById[apiId]; - if (apiDef != null) { - records.push( - ...this.generateAlgoliaSearchRecordsForApiDefinition( - apiDef, - context.withPathPart( - compact({ - name: api.title, - urlSlug: api.urlSlug, - skipUrlSlug: api.skipUrlSlug || undefined, - }), - ), - ), - ); - } - - if (item.changelog != null) { - records.push( - ...this.generateAlgoliaSearchRecordsForChangelogSection( - item.changelog, - context, - `${api.title} Changelog`, - ), - ); - } - - return records; + if (item.changelog != null) { + records.push( + ...this.generateAlgoliaSearchRecordsForChangelogSection( + item.changelog, + context, + `${api.title} Changelog` + ) + ); } - protected parseMarkdownItem( - rawMarkdown: string, - breadcrumbs: BreadcrumbsInfo[], - indexSegment: IndexSegment, - rawSlug: string, - title: string, - ): AlgoliaSearchRecord[] { - const version = - indexSegment.type === "versioned" - ? { - id: indexSegment.version.id, - slug: FernNavigation.V1.Slug(indexSegment.version.urlSlug ?? indexSegment.version.id), - } - : undefined; - const fdrSlug = FernNavigation.V1.Slug(rawSlug); - - // New markdown processing method - const { frontmatter } = getFrontmatter(rawMarkdown); - const markdownTree = getMarkdownSectionTree(rawMarkdown, title); - const markdownSectionRecords = getMarkdownSections(markdownTree, breadcrumbs, indexSegment.id, fdrSlug).map( - compact, - ); + return records; + } - markdownSectionRecords.push( - compact({ - type: "page-v4", - objectID: uuid(), - title: frontmatter.title ?? title, - description: truncateToBytes(markdownTree.content, 50 * 1000), - breadcrumbs, - slug: fdrSlug, - version, - indexSegmentId: indexSegment.id, - }), - ); + protected parseMarkdownItem( + rawMarkdown: string, + breadcrumbs: BreadcrumbsInfo[], + indexSegment: IndexSegment, + rawSlug: string, + title: string + ): AlgoliaSearchRecord[] { + const version = + indexSegment.type === "versioned" + ? { + id: indexSegment.version.id, + slug: FernNavigation.V1.Slug( + indexSegment.version.urlSlug ?? indexSegment.version.id + ), + } + : undefined; + const fdrSlug = FernNavigation.V1.Slug(rawSlug); + + // New markdown processing method + const { frontmatter } = getFrontmatter(rawMarkdown); + const markdownTree = getMarkdownSectionTree(rawMarkdown, title); + const markdownSectionRecords = getMarkdownSections( + markdownTree, + breadcrumbs, + indexSegment.id, + fdrSlug + ).map(compact); + + markdownSectionRecords.push( + compact({ + type: "page-v4", + objectID: uuid(), + title: frontmatter.title ?? title, + description: truncateToBytes(markdownTree.content, 50 * 1000), + breadcrumbs, + slug: fdrSlug, + version, + indexSegmentId: indexSegment.id, + }) + ); + + return markdownSectionRecords; + } - return markdownSectionRecords; + protected generateAlgoliaSectionRecordsForPageNavigationItem( + item: DocsV1Db.NavigationItem.Page, + context: NavigationContext + ): AlgoliaSearchRecord[] { + if (item.hidden) { + return []; } - protected generateAlgoliaSectionRecordsForPageNavigationItem( - item: DocsV1Db.NavigationItem.Page, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - if (item.hidden) { - return []; - } + const page = item; + const pageContent = this.config.docsDefinition.pages[page.id]; + if (pageContent == null) { + return []; + } - const page = item; - const pageContent = this.config.docsDefinition.pages[page.id]; - if (pageContent == null) { + const { indexSegment } = context; + const pageContext = + page.fullSlug != null + ? context.withFullSlug(page.fullSlug) + : context.withPathPart({ + name: page.title, + urlSlug: page.urlSlug, + skipUrlSlug: undefined, + }); + + const slug = pageContext.path; + + const markdownSectionRecords: AlgoliaSearchRecord[] = + this.parseMarkdownItem( + pageContent.markdown, + [], + indexSegment, + slug, + page.title + ); + + const processedContent = convertMarkdownToText(pageContent.markdown); + + return markdownSectionRecords.concat([ + compact({ + type: "page-v2", + objectID: uuid(), + title: page.title, // TODO: parse from frontmatter? + // TODO: Set to something more than 10kb on prod + // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ + content: truncateToBytes(processedContent, 50 * 1000), + path: { + parts: pageContext.pathParts, + }, + version: + indexSegment.type === "versioned" + ? { + id: indexSegment.version.id, + urlSlug: + indexSegment.version.urlSlug ?? indexSegment.version.id, + } + : undefined, + indexSegmentId: indexSegment.id, + }), + ]); + } + + protected override generateAlgoliaSearchRecordsForUnversionedTabbedNavigationConfig( + config: DocsV1Db.UnversionedTabbedNavigationConfig, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const records = + config.tabsV2?.flatMap((tab) => { + switch (tab.type) { + case "group": + return tab.items.flatMap((item) => + this.generateAlgoliaSearchRecordsForNavigationItem( + item, + context.withPathPart({ + name: tab.title, + urlSlug: tab.urlSlug, + skipUrlSlug: tab.skipUrlSlug, + }) + ) + ); + case "changelog": + return this.generateAlgoliaSearchRecordsForChangelogSection( + tab, + context.withPathPart({ + name: tab.title ?? "Changelog", + urlSlug: tab.urlSlug, + skipUrlSlug: undefined, + }) + ).concat( + this.generateAlgoliaSearchRecordsForChangelogSectionV2( + tab, + context.withPathPart({ + name: tab.title ?? "Changelog", + urlSlug: tab.urlSlug, + skipUrlSlug: undefined, + }) + ) + ); + default: return []; } - - const { indexSegment } = context; - const pageContext = - page.fullSlug != null - ? context.withFullSlug(page.fullSlug) - : context.withPathPart({ - name: page.title, - urlSlug: page.urlSlug, - skipUrlSlug: undefined, - }); - - const slug = pageContext.path; - - const markdownSectionRecords: AlgoliaSearchRecord[] = this.parseMarkdownItem( - pageContent.markdown, - [], - indexSegment, - slug, - page.title, + }) ?? + config.tabs?.map((tab) => + visitDbNavigationTab(tab, { + group: (group) => { + const tabRecords = + group.items?.map((item) => + this.generateAlgoliaSearchRecordsForNavigationItem( + item, + context.withPathPart({ + name: tab.title, + urlSlug: group.urlSlug, + skipUrlSlug: undefined, + }) + ) + ) ?? []; + return tabRecords.flat(1); + }, + link: () => [], + }) + ) ?? + []; + return records.flat(1); + } + + // Main Entrypoint Function + protected override generateAlgoliaSearchRecordsForNavigationItem( + item: DocsV1Db.NavigationItem, + context: NavigationContext + ): AlgoliaSearchRecord[] { + switch (item.type) { + case "section": + return this.generateAlgoliaSearchRecordsForSectionNavigationItem( + item, + context ); + case "api": + return this.generateAlgoliaSectionRecordsForApiNavigationItem( + item, + context + ); + case "page": + return this.generateAlgoliaSectionRecordsForPageNavigationItem( + item, + context + ); + case "link": + return []; + case "changelog": + return this.generateAlgoliaSearchRecordsForChangelogSection( + item, + context + ).concat( + this.generateAlgoliaSearchRecordsForChangelogSectionV2(item, context) + ); + case "changelogV3": + return this.generateAlgoliaSearchRecordsForChangelogNode( + item.node, + context + ).concat( + this.generateAlgoliaSearchRecordsForChangelogNodeV2( + item.node, + context + ) + ); + case "apiV2": + return this.generateAlgoliaSearchRecordsForApiReferenceNode( + item.node, + context + ).concat( + this.generateAlgoliaSearchRecordsForApiReferenceNodeV2( + item.node, + context + ) + ); + default: + assertNever(item); + } + } + + protected addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields: AlgoliaSearchRecord[], + typeReferences: TypeReferenceWithMetadata[], + parameterWithDescriptionAndAvailability: { + key: string; + description: string | undefined; + availability: FdrAPI.Availability | undefined; + }, + version: Algolia.AlgoliaRecordVersionV3 | undefined, + indexSegmentId: Algolia.IndexSegmentId, + endpoint: APIV1Read.EndpointDefinition, + anchorIdParts: string[], + node: FernNavigation.V1.EndpointNode, + parentBreadcrumbs: BreadcrumbsInfo[], + type: APIV1Read.TypeReference + ) { + const slug = anchorIdToSlug(node, anchorIdParts); + const breadcrumbs = parentBreadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); - const processedContent = convertMarkdownToText(pageContent.markdown); + fields.push({ + objectID: uuid(), + type: "endpoint-field-v1", + title: parameterWithDescriptionAndAvailability.key, + description: parameterWithDescriptionAndAvailability.description, + availability: parameterWithDescriptionAndAvailability.availability, + breadcrumbs, + slug, + version, + indexSegmentId, + method: endpoint.method, + endpointPath: endpoint.path.parts, + isResponseStream: node.isResponseStream, + extends: undefined, + }); + if (type.type === "id") { + typeReferences.push({ + reference: type, + anchorIdParts, + breadcrumbs, + slugPrefix: slug, + version, + indexSegmentId, + method: endpoint.method, + endpointPath: endpoint.path.parts, + isResponseStream: node.isResponseStream, + propertyKey: parameterWithDescriptionAndAvailability.key, + type: "endpoint-field-v1", + }); + } + } + + protected addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields: AlgoliaSearchRecord[], + typeReferences: TypeReferenceWithMetadata[], + parameterWithDescriptionAndAvailability: { + key: string; + description: string | undefined; + availability: FdrAPI.Availability | undefined; + }, + version: Algolia.AlgoliaRecordVersionV3 | undefined, + indexSegmentId: Algolia.IndexSegmentId, + websocket: APIV1Read.WebSocketChannel, + anchorIdParts: string[], + node: FernNavigation.V1.WebSocketNode, + parentBreadcrumbs: BreadcrumbsInfo[], + maybeTypeReference: APIV1Read.TypeReference + ) { + const slug = anchorIdToSlug(node, anchorIdParts); + const breadcrumbs = parentBreadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); - return markdownSectionRecords.concat([ - compact({ - type: "page-v2", - objectID: uuid(), - title: page.title, // TODO: parse from frontmatter? - // TODO: Set to something more than 10kb on prod - // See: https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-/ - content: truncateToBytes(processedContent, 50 * 1000), - path: { - parts: pageContext.pathParts, - }, - version: - indexSegment.type === "versioned" - ? { - id: indexSegment.version.id, - urlSlug: indexSegment.version.urlSlug ?? indexSegment.version.id, - } - : undefined, - indexSegmentId: indexSegment.id, - }), - ]); + fields.push({ + objectID: uuid(), + type: "websocket-field-v1", + title: parameterWithDescriptionAndAvailability.key, + description: parameterWithDescriptionAndAvailability.description, + availability: parameterWithDescriptionAndAvailability.availability, + breadcrumbs, + slug, + version, + indexSegmentId, + endpointPath: websocket.path.parts, + extends: undefined, + }); + if (maybeTypeReference.type === "id") { + typeReferences.push({ + reference: maybeTypeReference, + anchorIdParts, + breadcrumbs, + slugPrefix: slug, + version, + indexSegmentId, + method: "GET", + endpointPath: websocket.path.parts, + propertyKey: parameterWithDescriptionAndAvailability.key, + type: "websocket-field-v1", + }); } + } + + protected addWebhookFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields: AlgoliaSearchRecord[], + typeReferences: TypeReferenceWithMetadata[], + parameterWithDescriptionAndAvailability: { + key: string; + description: string | undefined; + availability: FdrAPI.Availability | undefined; + }, + version: Algolia.AlgoliaRecordVersionV3 | undefined, + indexSegmentId: Algolia.IndexSegmentId, + webhook: APIV1Read.WebhookDefinition, + anchorIdParts: string[], + node: FernNavigation.V1.WebhookNode, + parentBreadcrumbs: BreadcrumbsInfo[], + maybeTypeReference: APIV1Read.TypeReference + ) { + const slug = anchorIdToSlug(node, anchorIdParts); + const breadcrumbs = parentBreadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); - protected override generateAlgoliaSearchRecordsForUnversionedTabbedNavigationConfig( - config: DocsV1Db.UnversionedTabbedNavigationConfig, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const records = - config.tabsV2?.flatMap((tab) => { - switch (tab.type) { - case "group": - return tab.items.flatMap((item) => - this.generateAlgoliaSearchRecordsForNavigationItem( - item, - context.withPathPart({ - name: tab.title, - urlSlug: tab.urlSlug, - skipUrlSlug: tab.skipUrlSlug, - }), - ), - ); - case "changelog": - return this.generateAlgoliaSearchRecordsForChangelogSection( - tab, - context.withPathPart({ - name: tab.title ?? "Changelog", - urlSlug: tab.urlSlug, - skipUrlSlug: undefined, - }), - ).concat( - this.generateAlgoliaSearchRecordsForChangelogSectionV2( - tab, - context.withPathPart({ - name: tab.title ?? "Changelog", - urlSlug: tab.urlSlug, - skipUrlSlug: undefined, - }), - ), - ); - default: - return []; - } - }) ?? - config.tabs?.map((tab) => - visitDbNavigationTab(tab, { - group: (group) => { - const tabRecords = - group.items?.map((item) => - this.generateAlgoliaSearchRecordsForNavigationItem( - item, - context.withPathPart({ - name: tab.title, - urlSlug: group.urlSlug, - skipUrlSlug: undefined, - }), - ), - ) ?? []; - return tabRecords.flat(1); - }, - link: () => [], - }), - ) ?? - []; - return records.flat(1); + fields.push({ + objectID: uuid(), + type: "webhook-field-v1", + title: parameterWithDescriptionAndAvailability.key, + description: parameterWithDescriptionAndAvailability.description, + availability: parameterWithDescriptionAndAvailability.availability, + breadcrumbs, + slug, + version, + indexSegmentId, + method: webhook.method, + endpointPath: webhook.path.map((path) => ({ + type: "literal", + value: path, + })), + extends: undefined, + }); + if (maybeTypeReference.type === "id") { + typeReferences.push({ + reference: maybeTypeReference, + anchorIdParts, + breadcrumbs, + slugPrefix: slug, + version, + indexSegmentId, + method: webhook.method, + endpointPath: webhook.path.map((path) => ({ + type: "literal", + value: path, + })), + propertyKey: parameterWithDescriptionAndAvailability.key, + type: "websocket-field-v1", + }); + } + } + + protected generateAlgoliaSearchRecordsForApiReferenceNodeV2( + root: FernNavigation.V1.ApiReferenceNode, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const api = this.config.apiDefinitionsById[root.apiDefinitionId]; + if (api == null) { + LOGGER.error( + "Failed to find API definition for API reference node. id=", + root.apiDefinitionId + ); } + const holder = + api != null + ? FernNavigation.ApiDefinitionHolder.create( + convertDbAPIDefinitionToRead(api) + ) + : undefined; + const records: AlgoliaSearchRecord[] = []; + + const baseBreadcrumbs = context.pathParts.map((part) => ({ + title: part.name, + slug: part.urlSlug, + })); + + const version = + context.indexSegment.type === "versioned" + ? ({ + id: context.indexSegment.version.id, + slug: FernNavigation.V1.Slug( + context.indexSegment.version.urlSlug ?? + context.indexSegment.version.id + ), + } satisfies Algolia.AlgoliaRecordVersionV3) + : undefined; + + FernNavigation.V1.traverseDF(root, (node, parents) => { + if (!FernNavigation.V1.hasMetadata(node)) { + return; + } + + if (node.hidden) { + return "skip"; + } + + if (FernNavigation.V1.isApiLeaf(node)) { + const indexSegmentId = context.indexSegment.id; + const breadcrumbs = toBreadcrumbs( + baseBreadcrumbs.concat({ + title: node.title, + slug: node.slug, + }), + parents + ); + visitDiscriminatedUnion(node)._visit({ + endpoint: (node) => { + const endpoint = holder?.endpoints.get(node.endpointId); + if (endpoint == null) { + LOGGER.error( + "Failed to find endpoint for API reference node.", + node + ); + return; + } - // Main Entrypoint Function - protected override generateAlgoliaSearchRecordsForNavigationItem( - item: DocsV1Db.NavigationItem, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - switch (item.type) { - case "section": - return this.generateAlgoliaSearchRecordsForSectionNavigationItem(item, context); - case "api": - return this.generateAlgoliaSectionRecordsForApiNavigationItem(item, context); - case "page": - return this.generateAlgoliaSectionRecordsForPageNavigationItem(item, context); - case "link": - return []; - case "changelog": - return this.generateAlgoliaSearchRecordsForChangelogSection(item, context).concat( - this.generateAlgoliaSearchRecordsForChangelogSectionV2(item, context), + // this is a hack to include the endpoint request/response json in the search index + // and potentially use it for conversational AI in the future. + // this needs to be rewritten as a template, with proper markdown formatting + snapshot testing. + // also, the content is potentially trimmed to 10kb. + const fields: AlgoliaSearchRecord[] = []; + + const typeReferences: TypeReferenceWithMetadata[] = []; + + if (endpoint.headers != null && endpoint.headers.length > 0) { + endpoint.headers.forEach((header) => { + this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + header, + version, + indexSegmentId, + endpoint, + ["request", "header", header.key], + node, + breadcrumbs, + header.type ); - case "changelogV3": - return this.generateAlgoliaSearchRecordsForChangelogNode(item.node, context).concat( - this.generateAlgoliaSearchRecordsForChangelogNodeV2(item.node, context), + }); + } + + if (endpoint.path.pathParameters.length > 0) { + endpoint.path.pathParameters.forEach((param) => { + this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + param, + version, + indexSegmentId, + endpoint, + ["request", "path", param.key], + node, + breadcrumbs, + param.type ); - case "apiV2": - return this.generateAlgoliaSearchRecordsForApiReferenceNode(item.node, context).concat( - this.generateAlgoliaSearchRecordsForApiReferenceNodeV2(item.node, context), + }); + } + + if (endpoint.queryParameters.length > 0) { + endpoint.queryParameters.forEach((param) => { + this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + param, + version, + indexSegmentId, + endpoint, + ["request", "query", param.key], + node, + breadcrumbs, + param.type ); - default: - assertNever(item); - } - } + }); + } - protected addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields: AlgoliaSearchRecord[], - typeReferences: TypeReferenceWithMetadata[], - parameterWithDescriptionAndAvailability: { - key: string; - description: string | undefined; - availability: FdrAPI.Availability | undefined; - }, - version: Algolia.AlgoliaRecordVersionV3 | undefined, - indexSegmentId: Algolia.IndexSegmentId, - endpoint: APIV1Read.EndpointDefinition, - anchorIdParts: string[], - node: FernNavigation.V1.EndpointNode, - parentBreadcrumbs: BreadcrumbsInfo[], - type: APIV1Read.TypeReference, - ) { - const slug = anchorIdToSlug(node, anchorIdParts); - const breadcrumbs = parentBreadcrumbs.concat(anchorIdParts.map((part) => ({ title: part, slug }))); - - fields.push({ - objectID: uuid(), - type: "endpoint-field-v1", - title: parameterWithDescriptionAndAvailability.key, - description: parameterWithDescriptionAndAvailability.description, - availability: parameterWithDescriptionAndAvailability.availability, - breadcrumbs, - slug, - version, - indexSegmentId, - method: endpoint.method, - endpointPath: endpoint.path.parts, - isResponseStream: node.isResponseStream, - extends: undefined, - }); - if (type.type === "id") { - typeReferences.push({ - reference: type, - anchorIdParts, - breadcrumbs, - slugPrefix: slug, + if (endpoint.request != null) { + if (endpoint.request.type.type === "reference") { + const anchorIdParts = ["request", "body"]; + const slug = anchorIdToSlug(node, anchorIdParts); + const fieldBreadcrumbs = breadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); + typeReferences.push({ + reference: endpoint.request.type.value, + anchorIdParts, + breadcrumbs: fieldBreadcrumbs, + slugPrefix: slug, + version, + indexSegmentId, + method: endpoint.method, + endpointPath: endpoint.path.parts, + isResponseStream: node.isResponseStream, + propertyKey: undefined, + type: "endpoint-field-v1", + }); + } else if (endpoint.request.type.type === "formData") { + endpoint.request.type.properties.forEach((property) => { + if (property.type === "bodyProperty") { + this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + property, + version, + indexSegmentId, + endpoint, + ["request", "body", property.key], + node, + breadcrumbs, + property.valueType + ); + } + }); + } else if (endpoint.request.type.type === "object") { + endpoint.request.type.properties.forEach((property) => { + this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + property, + version, + indexSegmentId, + endpoint, + ["request", "body", property.key], + node, + breadcrumbs, + property.valueType + ); + }); + } + } + + if (endpoint.response != null) { + if (endpoint.response.type.type === "reference") { + const anchorIdParts = ["response", "body"]; + const slug = anchorIdToSlug(node, anchorIdParts); + const fieldBreadcrumbs = breadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); + typeReferences.push({ + reference: endpoint.response.type.value, + anchorIdParts, + breadcrumbs: fieldBreadcrumbs, + slugPrefix: slug, + version, + indexSegmentId, + method: endpoint.method, + endpointPath: endpoint.path.parts, + isResponseStream: node.isResponseStream, + propertyKey: undefined, + type: "endpoint-field-v1", + }); + } else if (endpoint.response.type.type === "object") { + endpoint.response.type.properties.forEach((property) => { + this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + property, + version, + indexSegmentId, + endpoint, + ["response", "body", property.key], + node, + breadcrumbs, + property.valueType + ); + }); + } + } + + records.push(...fields.map(compact)); + records.push( + ...this.collectReferencedTypesToContentV2( + typeReferences, + holder?.api.types ?? {} + ).map(compact) + ); + records.push( + compact({ + type: "endpoint-v4", + objectID: uuid(), + title: node.title, + description: endpoint.description, + breadcrumbs: toBreadcrumbs(breadcrumbs, parents), + slug: node.slug, version, - indexSegmentId, + indexSegmentId: context.indexSegment.id, method: endpoint.method, endpointPath: endpoint.path.parts, isResponseStream: node.isResponseStream, - propertyKey: parameterWithDescriptionAndAvailability.key, - type: "endpoint-field-v1", - }); - } - } + }) + ); + }, + webSocket: (node) => { + const ws = holder?.webSockets.get(node.webSocketId); + if (ws == null) { + LOGGER.error( + "Failed to find websocket for API reference node.", + node + ); + return; + } - protected addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields: AlgoliaSearchRecord[], - typeReferences: TypeReferenceWithMetadata[], - parameterWithDescriptionAndAvailability: { - key: string; - description: string | undefined; - availability: FdrAPI.Availability | undefined; - }, - version: Algolia.AlgoliaRecordVersionV3 | undefined, - indexSegmentId: Algolia.IndexSegmentId, - websocket: APIV1Read.WebSocketChannel, - anchorIdParts: string[], - node: FernNavigation.V1.WebSocketNode, - parentBreadcrumbs: BreadcrumbsInfo[], - maybeTypeReference: APIV1Read.TypeReference, - ) { - const slug = anchorIdToSlug(node, anchorIdParts); - const breadcrumbs = parentBreadcrumbs.concat(anchorIdParts.map((part) => ({ title: part, slug }))); - - fields.push({ - objectID: uuid(), - type: "websocket-field-v1", - title: parameterWithDescriptionAndAvailability.key, - description: parameterWithDescriptionAndAvailability.description, - availability: parameterWithDescriptionAndAvailability.availability, - breadcrumbs, - slug, - version, - indexSegmentId, - endpointPath: websocket.path.parts, - extends: undefined, - }); - if (maybeTypeReference.type === "id") { - typeReferences.push({ - reference: maybeTypeReference, - anchorIdParts, - breadcrumbs, - slugPrefix: slug, + const typeReferences: TypeReferenceWithMetadata[] = []; + + const fields: AlgoliaSearchRecord[] = []; + + if (ws.headers.length > 0) { + ws.headers.forEach((param) => { + this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + param, + version, + indexSegmentId, + ws, + ["request", "header", param.key], + node, + breadcrumbs, + param.type + ); + }); + } + + if (ws.path.pathParameters.length > 0) { + ws.path.pathParameters.forEach((param) => { + this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + param, + version, + indexSegmentId, + ws, + ["request", "path", param.key], + node, + breadcrumbs, + param.type + ); + }); + } + + if (ws.queryParameters.length > 0) { + ws.queryParameters.forEach((param) => { + this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + param, + version, + indexSegmentId, + ws, + ["request", "query", param.key], + node, + breadcrumbs, + param.type + ); + }); + } + + if (ws.messages.length > 0) { + ws.messages.forEach((message) => { + const messageType = + message.origin === "server" ? "receive" : "send"; + const slug = anchorIdToSlug(node, [messageType]); + if (message.body.type === "reference") { + const anchorIdParts = [ + node.title, + message.origin === "server" ? "receive" : "send", + ]; + if (message.displayName != null) { + anchorIdParts.push(message.displayName); + } + const fieldBreadcrumbs = breadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); + typeReferences.push({ + reference: message.body.value, + anchorIdParts, + breadcrumbs: fieldBreadcrumbs, + slugPrefix: anchorIdToSlug(node, anchorIdParts), + version, + indexSegmentId, + endpointPath: ws.path.parts, + type: "websocket-field-v1", + propertyKey: undefined, + method: "GET", + }); + } else if (message.body.type === "object") { + message.body.properties.forEach((property) => { + const anchorIdParts = [ + node.title, + message.origin === "server" ? "receive" : "send", + ]; + if (message.displayName != null) { + anchorIdParts.push(message.displayName); + } + anchorIdParts.push(property.key); + this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + property, + version, + indexSegmentId, + ws, + anchorIdParts, + node, + breadcrumbs, + property.valueType + ); + }); + } else { + assertNever(message.body); + } + }); + } + + records.push(...fields.map(compact)); + records.push( + ...this.collectReferencedTypesToContentV2( + typeReferences, + holder?.api.types ?? {} + ).map(compact) + ); + records.push( + compact({ + type: "websocket-v4", + objectID: uuid(), + title: node.title, + description: ws.description, + breadcrumbs: toBreadcrumbs(breadcrumbs, parents), + slug: node.slug, version, - indexSegmentId, - method: "GET", - endpointPath: websocket.path.parts, - propertyKey: parameterWithDescriptionAndAvailability.key, - type: "websocket-field-v1", - }); - } - } + indexSegmentId: context.indexSegment.id, + endpointPath: ws.path.parts, + }) + ); + }, + webhook: (node) => { + const webhook = holder?.webhooks.get(node.webhookId); + if (webhook == null) { + LOGGER.error( + "Failed to find webhook for API reference node.", + node + ); + return; + } - protected addWebhookFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields: AlgoliaSearchRecord[], - typeReferences: TypeReferenceWithMetadata[], - parameterWithDescriptionAndAvailability: { - key: string; - description: string | undefined; - availability: FdrAPI.Availability | undefined; - }, - version: Algolia.AlgoliaRecordVersionV3 | undefined, - indexSegmentId: Algolia.IndexSegmentId, - webhook: APIV1Read.WebhookDefinition, - anchorIdParts: string[], - node: FernNavigation.V1.WebhookNode, - parentBreadcrumbs: BreadcrumbsInfo[], - maybeTypeReference: APIV1Read.TypeReference, - ) { - const slug = anchorIdToSlug(node, anchorIdParts); - const breadcrumbs = parentBreadcrumbs.concat(anchorIdParts.map((part) => ({ title: part, slug }))); - - fields.push({ - objectID: uuid(), - type: "webhook-field-v1", - title: parameterWithDescriptionAndAvailability.key, - description: parameterWithDescriptionAndAvailability.description, - availability: parameterWithDescriptionAndAvailability.availability, - breadcrumbs, - slug, - version, - indexSegmentId, - method: webhook.method, - endpointPath: webhook.path.map((path) => ({ + const typeReferences: TypeReferenceWithMetadata[] = []; + const fields: AlgoliaSearchRecord[] = []; + const endpointPath: EndpointPathPart[] = webhook.path.map( + (path) => ({ type: "literal", value: path, - })), - extends: undefined, - }); - if (maybeTypeReference.type === "id") { - typeReferences.push({ - reference: maybeTypeReference, + }) + ); + + if (webhook.headers.length > 0) { + //TODO(rohin): check webhook anchor ids + webhook.headers.forEach((header) => { + this.addWebhookFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + header, + version, + indexSegmentId, + webhook, + ["request", "header", header.key], + node, + breadcrumbs, + header.type + ); + }); + } + + if (webhook.payload.type.type === "reference") { + const anchorIdParts = ["request", "body"]; + const slug = anchorIdToSlug(node, anchorIdParts); + const fieldBreadcrumbs = breadcrumbs.concat( + anchorIdParts.map((part) => ({ title: part, slug })) + ); + typeReferences.push({ + reference: webhook.payload.type.value, anchorIdParts, - breadcrumbs, + breadcrumbs: fieldBreadcrumbs, slugPrefix: slug, version, indexSegmentId, method: webhook.method, - endpointPath: webhook.path.map((path) => ({ - type: "literal", - value: path, - })), - propertyKey: parameterWithDescriptionAndAvailability.key, + endpointPath, + propertyKey: undefined, type: "websocket-field-v1", - }); - } - } - - protected generateAlgoliaSearchRecordsForApiReferenceNodeV2( - root: FernNavigation.V1.ApiReferenceNode, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const api = this.config.apiDefinitionsById[root.apiDefinitionId]; - if (api == null) { - LOGGER.error("Failed to find API definition for API reference node. id=", root.apiDefinitionId); - } - const holder = - api != null ? FernNavigation.ApiDefinitionHolder.create(convertDbAPIDefinitionToRead(api)) : undefined; - const records: AlgoliaSearchRecord[] = []; - - const baseBreadcrumbs = context.pathParts.map((part) => ({ - title: part.name, - slug: part.urlSlug, - })); - - const version = - context.indexSegment.type === "versioned" - ? ({ - id: context.indexSegment.version.id, - slug: FernNavigation.V1.Slug( - context.indexSegment.version.urlSlug ?? context.indexSegment.version.id, - ), - } satisfies Algolia.AlgoliaRecordVersionV3) - : undefined; - - FernNavigation.V1.traverseDF(root, (node, parents) => { - if (!FernNavigation.V1.hasMetadata(node)) { - return; - } - - if (node.hidden) { - return "skip"; - } - - if (FernNavigation.V1.isApiLeaf(node)) { - const indexSegmentId = context.indexSegment.id; - const breadcrumbs = toBreadcrumbs( - baseBreadcrumbs.concat({ - title: node.title, - slug: node.slug, - }), - parents, - ); - visitDiscriminatedUnion(node)._visit({ - endpoint: (node) => { - const endpoint = holder?.endpoints.get(node.endpointId); - if (endpoint == null) { - LOGGER.error("Failed to find endpoint for API reference node.", node); - return; - } - - // this is a hack to include the endpoint request/response json in the search index - // and potentially use it for conversational AI in the future. - // this needs to be rewritten as a template, with proper markdown formatting + snapshot testing. - // also, the content is potentially trimmed to 10kb. - const fields: AlgoliaSearchRecord[] = []; - - const typeReferences: TypeReferenceWithMetadata[] = []; - - if (endpoint.headers != null && endpoint.headers.length > 0) { - endpoint.headers.forEach((header) => { - this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - header, - version, - indexSegmentId, - endpoint, - ["request", "header", header.key], - node, - breadcrumbs, - header.type, - ); - }); - } - - if (endpoint.path.pathParameters.length > 0) { - endpoint.path.pathParameters.forEach((param) => { - this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - param, - version, - indexSegmentId, - endpoint, - ["request", "path", param.key], - node, - breadcrumbs, - param.type, - ); - }); - } - - if (endpoint.queryParameters.length > 0) { - endpoint.queryParameters.forEach((param) => { - this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - param, - version, - indexSegmentId, - endpoint, - ["request", "query", param.key], - node, - breadcrumbs, - param.type, - ); - }); - } - - if (endpoint.request != null) { - if (endpoint.request.type.type === "reference") { - const anchorIdParts = ["request", "body"]; - const slug = anchorIdToSlug(node, anchorIdParts); - const fieldBreadcrumbs = breadcrumbs.concat( - anchorIdParts.map((part) => ({ title: part, slug })), - ); - typeReferences.push({ - reference: endpoint.request.type.value, - anchorIdParts, - breadcrumbs: fieldBreadcrumbs, - slugPrefix: slug, - version, - indexSegmentId, - method: endpoint.method, - endpointPath: endpoint.path.parts, - isResponseStream: node.isResponseStream, - propertyKey: undefined, - type: "endpoint-field-v1", - }); - } else if (endpoint.request.type.type === "formData") { - endpoint.request.type.properties.forEach((property) => { - if (property.type === "bodyProperty") { - this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - property, - version, - indexSegmentId, - endpoint, - ["request", "body", property.key], - node, - breadcrumbs, - property.valueType, - ); - } - }); - } else if (endpoint.request.type.type === "object") { - endpoint.request.type.properties.forEach((property) => { - this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - property, - version, - indexSegmentId, - endpoint, - ["request", "body", property.key], - node, - breadcrumbs, - property.valueType, - ); - }); - } - } - - if (endpoint.response != null) { - if (endpoint.response.type.type === "reference") { - const anchorIdParts = ["response", "body"]; - const slug = anchorIdToSlug(node, anchorIdParts); - const fieldBreadcrumbs = breadcrumbs.concat( - anchorIdParts.map((part) => ({ title: part, slug })), - ); - typeReferences.push({ - reference: endpoint.response.type.value, - anchorIdParts, - breadcrumbs: fieldBreadcrumbs, - slugPrefix: slug, - version, - indexSegmentId, - method: endpoint.method, - endpointPath: endpoint.path.parts, - isResponseStream: node.isResponseStream, - propertyKey: undefined, - type: "endpoint-field-v1", - }); - } else if (endpoint.response.type.type === "object") { - endpoint.response.type.properties.forEach((property) => { - this.addEndpointFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - property, - version, - indexSegmentId, - endpoint, - ["response", "body", property.key], - node, - breadcrumbs, - property.valueType, - ); - }); - } - } - - records.push(...fields.map(compact)); - records.push( - ...this.collectReferencedTypesToContentV2(typeReferences, holder?.api.types ?? {}).map( - compact, - ), - ); - records.push( - compact({ - type: "endpoint-v4", - objectID: uuid(), - title: node.title, - description: endpoint.description, - breadcrumbs: toBreadcrumbs(breadcrumbs, parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - method: endpoint.method, - endpointPath: endpoint.path.parts, - isResponseStream: node.isResponseStream, - }), - ); - }, - webSocket: (node) => { - const ws = holder?.webSockets.get(node.webSocketId); - if (ws == null) { - LOGGER.error("Failed to find websocket for API reference node.", node); - return; - } - - const typeReferences: TypeReferenceWithMetadata[] = []; - - const fields: AlgoliaSearchRecord[] = []; - - if (ws.headers.length > 0) { - ws.headers.forEach((param) => { - this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - param, - version, - indexSegmentId, - ws, - ["request", "header", param.key], - node, - breadcrumbs, - param.type, - ); - }); - } - - if (ws.path.pathParameters.length > 0) { - ws.path.pathParameters.forEach((param) => { - this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - param, - version, - indexSegmentId, - ws, - ["request", "path", param.key], - node, - breadcrumbs, - param.type, - ); - }); - } - - if (ws.queryParameters.length > 0) { - ws.queryParameters.forEach((param) => { - this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - param, - version, - indexSegmentId, - ws, - ["request", "query", param.key], - node, - breadcrumbs, - param.type, - ); - }); - } - - if (ws.messages.length > 0) { - ws.messages.forEach((message) => { - const messageType = message.origin === "server" ? "receive" : "send"; - const slug = anchorIdToSlug(node, [messageType]); - if (message.body.type === "reference") { - const anchorIdParts = [ - node.title, - message.origin === "server" ? "receive" : "send", - ]; - if (message.displayName != null) { - anchorIdParts.push(message.displayName); - } - const fieldBreadcrumbs = breadcrumbs.concat( - anchorIdParts.map((part) => ({ title: part, slug })), - ); - typeReferences.push({ - reference: message.body.value, - anchorIdParts, - breadcrumbs: fieldBreadcrumbs, - slugPrefix: anchorIdToSlug(node, anchorIdParts), - version, - indexSegmentId, - endpointPath: ws.path.parts, - type: "websocket-field-v1", - propertyKey: undefined, - method: "GET", - }); - } else if (message.body.type === "object") { - message.body.properties.forEach((property) => { - const anchorIdParts = [ - node.title, - message.origin === "server" ? "receive" : "send", - ]; - if (message.displayName != null) { - anchorIdParts.push(message.displayName); - } - anchorIdParts.push(property.key); - this.addWebsocketFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - property, - version, - indexSegmentId, - ws, - anchorIdParts, - node, - breadcrumbs, - property.valueType, - ); - }); - } else { - assertNever(message.body); - } - }); - } - - records.push(...fields.map(compact)); - records.push( - ...this.collectReferencedTypesToContentV2(typeReferences, holder?.api.types ?? {}).map( - compact, - ), - ); - records.push( - compact({ - type: "websocket-v4", - objectID: uuid(), - title: node.title, - description: ws.description, - breadcrumbs: toBreadcrumbs(breadcrumbs, parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - endpointPath: ws.path.parts, - }), - ); - }, - webhook: (node) => { - const webhook = holder?.webhooks.get(node.webhookId); - if (webhook == null) { - LOGGER.error("Failed to find webhook for API reference node.", node); - return; - } - - const typeReferences: TypeReferenceWithMetadata[] = []; - const fields: AlgoliaSearchRecord[] = []; - const endpointPath: EndpointPathPart[] = webhook.path.map((path) => ({ - type: "literal", - value: path, - })); - - if (webhook.headers.length > 0) { - //TODO(rohin): check webhook anchor ids - webhook.headers.forEach((header) => { - this.addWebhookFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - header, - version, - indexSegmentId, - webhook, - ["request", "header", header.key], - node, - breadcrumbs, - header.type, - ); - }); - } - - if (webhook.payload.type.type === "reference") { - const anchorIdParts = ["request", "body"]; - const slug = anchorIdToSlug(node, anchorIdParts); - const fieldBreadcrumbs = breadcrumbs.concat( - anchorIdParts.map((part) => ({ title: part, slug })), - ); - typeReferences.push({ - reference: webhook.payload.type.value, - anchorIdParts, - breadcrumbs: fieldBreadcrumbs, - slugPrefix: slug, - version, - indexSegmentId, - method: webhook.method, - endpointPath, - propertyKey: undefined, - type: "websocket-field-v1", - }); - } else if (webhook.payload.type.type === "object") { - webhook.payload.type.properties.forEach((property) => { - this.addWebhookFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( - fields, - typeReferences, - property, - version, - indexSegmentId, - webhook, - ["request", "body", property.key], - node, - breadcrumbs, - property.valueType, - ); - }); - } else { - assertNever(webhook.payload.type); - } - - records.push(...fields.map(compact)); - records.push( - ...this.collectReferencedTypesToContentV2(typeReferences, holder?.api.types ?? {}).map( - compact, - ), - ); - records.push( - compact({ - type: "webhook-v4", - objectID: uuid(), - title: node.title, - description: webhook.description, - breadcrumbs: toBreadcrumbs(breadcrumbs, parents), - slug: node.slug, - version, - indexSegmentId: context.indexSegment.id, - method: webhook.method, - endpointPath, - }), - ); - }, - }); - } else if (FernNavigation.V1.hasMarkdown(node)) { - const pageId = FernNavigation.V1.getPageId(node); - if (pageId == null) { - return; - } - - const md = this.config.docsDefinition.pages[pageId]?.markdown; - if (md == null) { - LOGGER.error("Failed to find markdown for node", node); - return; - } - - const markdownSectionRecords = this.parseMarkdownItem( - md, - toBreadcrumbs([], parents), - context.indexSegment, - node.slug, - node.title, + }); + } else if (webhook.payload.type.type === "object") { + webhook.payload.type.properties.forEach((property) => { + this.addWebhookFieldAndMaybeTypeReferenceToAlgoliaSearchRecords( + fields, + typeReferences, + property, + version, + indexSegmentId, + webhook, + ["request", "body", property.key], + node, + breadcrumbs, + property.valueType ); - - records.push(...markdownSectionRecords); + }); + } else { + assertNever(webhook.payload.type); } - return; + + records.push(...fields.map(compact)); + records.push( + ...this.collectReferencedTypesToContentV2( + typeReferences, + holder?.api.types ?? {} + ).map(compact) + ); + records.push( + compact({ + type: "webhook-v4", + objectID: uuid(), + title: node.title, + description: webhook.description, + breadcrumbs: toBreadcrumbs(breadcrumbs, parents), + slug: node.slug, + version, + indexSegmentId: context.indexSegment.id, + method: webhook.method, + endpointPath, + }) + ); + }, }); + } else if (FernNavigation.V1.hasMarkdown(node)) { + const pageId = FernNavigation.V1.getPageId(node); + if (pageId == null) { + return; + } - return records; - } + const md = this.config.docsDefinition.pages[pageId]?.markdown; + if (md == null) { + LOGGER.error("Failed to find markdown for node", node); + return; + } - private collectReferencedTypeReferenceToContentV2( - id: APIV1Read.TypeReference.Id, - object: APIV1Read.TypeShape.Object_, - fields: AlgoliaSearchRecord[], - typeReferenceWithMetadata: TypeReferenceWithMetadata, - baseSlug: string, - additionalProperties: any, - types: Record, - visitedNodes: Set, - discriminatedUnionVariants: Set, - undiscriminatedUnionVariants: Set, - depth: number, - ) { - const referenceLeaves: TypeReferenceWithMetadata[] = []; - object.properties.forEach((property) => { - const slug = FernNavigation.V1.Slug(`${baseSlug}.${encodeURI(property.key)}`); - // If we see and object shape for a property, we need to recursively collect the underlying referenced types. - // If we see a reference or a container type, we will add it to the referenceLeaves to be processed in the next iteration. - switch (property.valueType.type) { - // here we check for reference types to process in the next iteration - case "id": - referenceLeaves.push({ - reference: property.valueType, - anchorIdParts: [...typeReferenceWithMetadata.anchorIdParts, property.key], - breadcrumbs: [ - ...typeReferenceWithMetadata.breadcrumbs, - { - title: property.key, - slug: `${baseSlug}.${encodeURI(property.key)}`, - }, - ], - slugPrefix: slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: property.key, - type: typeReferenceWithMetadata.type, - }); - break; - // here we check for container types to process in the next iteration - case "optional": - case "map": - case "list": - case "set": - referenceLeaves.push({ - reference: property.valueType, - anchorIdParts: [...typeReferenceWithMetadata.anchorIdParts], - breadcrumbs: [...typeReferenceWithMetadata.breadcrumbs], - slugPrefix: slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: property.key, - type: typeReferenceWithMetadata.type, - }); - break; - // If we see the property is a primitive or literal, we add it to our collection of algolia records. - case "primitive": - case "literal": - case "unknown": - fields.push({ - objectID: uuid(), - title: property.key, - description: property.description, - availability: property.availability, - breadcrumbs: typeReferenceWithMetadata.breadcrumbs.concat({ - title: property.key, - slug, - }), - slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - extends: object.extends, - ...additionalProperties, - }); - break; - default: { - assertNever(property.valueType); - } + const markdownSectionRecords = this.parseMarkdownItem( + md, + toBreadcrumbs([], parents), + context.indexSegment, + node.slug, + node.title + ); + + records.push(...markdownSectionRecords); + } + return; + }); + + return records; + } + + private collectReferencedTypeReferenceToContentV2( + id: APIV1Read.TypeReference.Id, + object: APIV1Read.TypeShape.Object_, + fields: AlgoliaSearchRecord[], + typeReferenceWithMetadata: TypeReferenceWithMetadata, + baseSlug: string, + additionalProperties: any, + types: Record, + visitedNodes: Set, + discriminatedUnionVariants: Set, + undiscriminatedUnionVariants: Set, + depth: number + ) { + const referenceLeaves: TypeReferenceWithMetadata[] = []; + object.properties.forEach((property) => { + const slug = FernNavigation.V1.Slug( + `${baseSlug}.${encodeURI(property.key)}` + ); + // If we see and object shape for a property, we need to recursively collect the underlying referenced types. + // If we see a reference or a container type, we will add it to the referenceLeaves to be processed in the next iteration. + switch (property.valueType.type) { + // here we check for reference types to process in the next iteration + case "id": + referenceLeaves.push({ + reference: property.valueType, + anchorIdParts: [ + ...typeReferenceWithMetadata.anchorIdParts, + property.key, + ], + breadcrumbs: [ + ...typeReferenceWithMetadata.breadcrumbs, + { + title: property.key, + slug: `${baseSlug}.${encodeURI(property.key)}`, + }, + ], + slugPrefix: slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: property.key, + type: typeReferenceWithMetadata.type, + }); + break; + // here we check for container types to process in the next iteration + case "optional": + case "map": + case "list": + case "set": + referenceLeaves.push({ + reference: property.valueType, + anchorIdParts: [...typeReferenceWithMetadata.anchorIdParts], + breadcrumbs: [...typeReferenceWithMetadata.breadcrumbs], + slugPrefix: slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: property.key, + type: typeReferenceWithMetadata.type, + }); + break; + // If we see the property is a primitive or literal, we add it to our collection of algolia records. + case "primitive": + case "literal": + case "unknown": + fields.push({ + objectID: uuid(), + title: property.key, + description: property.description, + availability: property.availability, + breadcrumbs: typeReferenceWithMetadata.breadcrumbs.concat({ + title: property.key, + slug, + }), + slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + extends: object.extends, + ...additionalProperties, + }); + break; + default: { + assertNever(property.valueType); + } + } + }); + // If we see an extension on the object, we need to process the internal types. + object.extends.forEach((extend) => { + referenceLeaves.push({ + reference: { type: "id", value: extend, default: undefined }, + anchorIdParts: typeReferenceWithMetadata.anchorIdParts, + breadcrumbs: typeReferenceWithMetadata.breadcrumbs, + slugPrefix: baseSlug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: undefined, + type: typeReferenceWithMetadata.type, + }); + }); + + fields.push( + ...this.collectReferencedTypesToContentV2( + referenceLeaves, + types, + new Set(visitedNodes).add(id.value), + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + } + + collectReferencedUndiscriminatedUnionToContentV2( + undiscriminatedUnion: APIV1Read.TypeShape.UndiscriminatedUnion, + fields: AlgoliaSearchRecord[], + typeReferenceWithMetadata: TypeReferenceWithMetadata, + baseSlug: string, + additionalProperties: any, + types: Record, + visitedNodes: Set, + discriminatedUnionVariants: Set, + undiscriminatedUnionVariants: Set, + depth: number + ) { + const referenceLeaves: TypeReferenceWithMetadata[] = []; + const newVariantSet = new Set(undiscriminatedUnionVariants); + undiscriminatedUnion.variants.forEach((variant, idx) => { + const title = + variant.displayName ?? + (variant.type.type === "id" + ? titleCase(types[variant.type.value]?.name ?? "") + : ""); + if (!undiscriminatedUnionVariants.has(title) || title == "") { + newVariantSet.add(title); + const slug = + title != "" + ? FernNavigation.V1.Slug(`${baseSlug}.${encodeURI(title)}`) + : baseSlug; + const anchorIdParts = + title != "" + ? [...typeReferenceWithMetadata.anchorIdParts, title] + : typeReferenceWithMetadata.anchorIdParts; + const breadcrumbs = + title != "" + ? [...typeReferenceWithMetadata.breadcrumbs, { title, slug }] + : typeReferenceWithMetadata.breadcrumbs; + // For undiscriminated unions, we need to check if there are any nested types that need to be processed. + switch (variant.type.type) { + case "id": + referenceLeaves.push({ + reference: variant.type, + anchorIdParts, + breadcrumbs, + slugPrefix: title != "" ? slug : baseSlug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: undefined, + type: typeReferenceWithMetadata.type, + }); + break; + // here we check for container types to process in the next iteration + case "optional": + referenceLeaves.push({ + reference: variant.type, + anchorIdParts, + breadcrumbs, + slugPrefix: slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: undefined, + type: typeReferenceWithMetadata.type, + }); + break; + case "map": + case "list": + case "set": { + const lastBreadcrumb = breadcrumbs.pop(); + const newSlug = `${slug}.${idx}`; + if (lastBreadcrumb != null) { + lastBreadcrumb.slug = newSlug; + breadcrumbs.push(lastBreadcrumb); } - }); - // If we see an extension on the object, we need to process the internal types. - object.extends.forEach((extend) => { referenceLeaves.push({ - reference: { type: "id", value: extend, default: undefined }, - anchorIdParts: typeReferenceWithMetadata.anchorIdParts, - breadcrumbs: typeReferenceWithMetadata.breadcrumbs, - slugPrefix: baseSlug, + reference: variant.type, + anchorIdParts, + breadcrumbs, + slugPrefix: newSlug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: undefined, + type: typeReferenceWithMetadata.type, + }); + break; + } + // If we see the variant is a primitive or literal, we add it to our collection of algolia records. + case "primitive": + case "literal": + case "unknown": + fields.push({ + objectID: uuid(), + title, + description: variant.description, + availability: variant.availability, + breadcrumbs: [ + ...typeReferenceWithMetadata.breadcrumbs, + { + title, + slug, + }, + ], + slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + extends: undefined, + ...additionalProperties, + }); + break; + default: + assertNever(variant.type); + } + } + }); + fields.push( + ...this.collectReferencedTypesToContentV2( + referenceLeaves, + types, + visitedNodes, + discriminatedUnionVariants, + newVariantSet, + depth + 1 + ) + ); + } + + private collectReferencedDiscriminatedUnionToContentV2( + discriminatedUnion: APIV1Read.TypeShape.DiscriminatedUnion, + fields: AlgoliaSearchRecord[], + typeReferenceWithMetadata: TypeReferenceWithMetadata, + baseSlug: string, + additionalProperties: any, + types: Record, + visitedNodes: Set, + discriminatedUnionVariants: Set, + undiscriminatedUnionVariants: Set, + depth: number + ) { + const referenceLeaves: TypeReferenceWithMetadata[] = []; + const newDiscriminatedUnionVariants = new Set(discriminatedUnionVariants); + + discriminatedUnion.variants.forEach((variant) => { + if (!discriminatedUnionVariants.has(variant.discriminantValue)) { + newDiscriminatedUnionVariants.add(variant.discriminantValue); + const title = + variant.discriminantValue ?? encodeURI(variant.displayName ?? ""); + const slug = FernNavigation.V1.Slug(`${baseSlug}.${title}`); + + // additional properties on the variant are the object shapes themselves, + // so we check for extension types here. + variant.additionalProperties.extends.forEach((extend) => { + referenceLeaves.push({ + reference: { type: "id", value: extend, default: undefined }, + anchorIdParts: typeReferenceWithMetadata.anchorIdParts, + breadcrumbs: [ + ...typeReferenceWithMetadata.breadcrumbs, + { title, slug }, + ], + slugPrefix: `${baseSlug}.${title}`, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: undefined, + type: typeReferenceWithMetadata.type, + }); + }); + variant.additionalProperties.properties.forEach((property) => { + // Here we check for references or container types to process in the next iteration. + switch (property.valueType.type) { + case "id": + referenceLeaves.push({ + reference: property.valueType, + anchorIdParts: [ + ...typeReferenceWithMetadata.anchorIdParts, + title, + property.key, + ], + breadcrumbs: [ + ...typeReferenceWithMetadata.breadcrumbs, + { title, slug }, + { + title: property.key, + slug: `${baseSlug}.${title}.${encodeURI(property.key)}`, + }, + ], + slugPrefix: `${baseSlug}.${title}.${encodeURI(property.key)}`, version: typeReferenceWithMetadata.version, indexSegmentId: typeReferenceWithMetadata.indexSegmentId, method: typeReferenceWithMetadata.method, endpointPath: typeReferenceWithMetadata.endpointPath, isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: undefined, + propertyKey: property.key, type: typeReferenceWithMetadata.type, - }); - }); - - fields.push( - ...this.collectReferencedTypesToContentV2( - referenceLeaves, - types, - new Set(visitedNodes).add(id.value), - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); - } - - collectReferencedUndiscriminatedUnionToContentV2( - undiscriminatedUnion: APIV1Read.TypeShape.UndiscriminatedUnion, - fields: AlgoliaSearchRecord[], - typeReferenceWithMetadata: TypeReferenceWithMetadata, - baseSlug: string, - additionalProperties: any, - types: Record, - visitedNodes: Set, - discriminatedUnionVariants: Set, - undiscriminatedUnionVariants: Set, - depth: number, - ) { - const referenceLeaves: TypeReferenceWithMetadata[] = []; - const newVariantSet = new Set(undiscriminatedUnionVariants); - undiscriminatedUnion.variants.forEach((variant, idx) => { - const title = - variant.displayName ?? - (variant.type.type === "id" ? titleCase(types[variant.type.value]?.name ?? "") : ""); - if (!undiscriminatedUnionVariants.has(title) || title == "") { - newVariantSet.add(title); - const slug = title != "" ? FernNavigation.V1.Slug(`${baseSlug}.${encodeURI(title)}`) : baseSlug; - const anchorIdParts = - title != "" - ? [...typeReferenceWithMetadata.anchorIdParts, title] - : typeReferenceWithMetadata.anchorIdParts; - const breadcrumbs = - title != "" - ? [...typeReferenceWithMetadata.breadcrumbs, { title, slug }] - : typeReferenceWithMetadata.breadcrumbs; - // For undiscriminated unions, we need to check if there are any nested types that need to be processed. - switch (variant.type.type) { - case "id": - referenceLeaves.push({ - reference: variant.type, - anchorIdParts, - breadcrumbs, - slugPrefix: title != "" ? slug : baseSlug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: undefined, - type: typeReferenceWithMetadata.type, - }); - break; - // here we check for container types to process in the next iteration - case "optional": - referenceLeaves.push({ - reference: variant.type, - anchorIdParts, - breadcrumbs, - slugPrefix: slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: undefined, - type: typeReferenceWithMetadata.type, - }); - break; - case "map": - case "list": - case "set": { - const lastBreadcrumb = breadcrumbs.pop(); - const newSlug = `${slug}.${idx}`; - if (lastBreadcrumb != null) { - lastBreadcrumb.slug = newSlug; - breadcrumbs.push(lastBreadcrumb); - } - referenceLeaves.push({ - reference: variant.type, - anchorIdParts, - breadcrumbs, - slugPrefix: newSlug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: undefined, - type: typeReferenceWithMetadata.type, - }); - break; - } - // If we see the variant is a primitive or literal, we add it to our collection of algolia records. - case "primitive": - case "literal": - case "unknown": - fields.push({ - objectID: uuid(), - title, - description: variant.description, - availability: variant.availability, - breadcrumbs: [ - ...typeReferenceWithMetadata.breadcrumbs, - { - title, - slug, - }, - ], - slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - extends: undefined, - ...additionalProperties, - }); - break; - default: - assertNever(variant.type); - } - } + }); + break; + // here we check for container types to process in the next iteration + case "optional": + case "map": + case "list": + case "set": + referenceLeaves.push({ + reference: property.valueType, + anchorIdParts: [ + ...typeReferenceWithMetadata.anchorIdParts, + title, + ], + breadcrumbs: [ + ...typeReferenceWithMetadata.breadcrumbs, + { title, slug }, + ], + slugPrefix: `${baseSlug}.${title}`, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + propertyKey: property.key, + type: typeReferenceWithMetadata.type, + }); + break; + // otherwise we check for primitive or literal types to add to our collection of algolia records. + case "primitive": + case "literal": + case "unknown": + fields.push({ + objectID: uuid(), + title: property.key, + description: property.description, + availability: property.availability, + breadcrumbs: typeReferenceWithMetadata.breadcrumbs.concat({ + title: property.key, + slug, + }), + slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + extends: variant.additionalProperties.extends, + ...additionalProperties, + }); + break; + default: + assertNever(property.valueType); + } }); - fields.push( - ...this.collectReferencedTypesToContentV2( - referenceLeaves, - types, - visitedNodes, - discriminatedUnionVariants, - newVariantSet, - depth + 1, - ), - ); + } + }); + + fields.push( + ...this.collectReferencedTypesToContentV2( + referenceLeaves, + types, + visitedNodes, + newDiscriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + } + + // The idea behind this function is to collect the smallest subset of records that capture all type information. + // + // To do this, we descend the type tree and resolve the leaf nodes and map them to records that have built up breadcrumbs, + // slugs, and other metadata that is useful for search indexing. + protected collectReferencedTypesToContentV2( + typeReferencesWithMetadata: TypeReferenceWithMetadata[], + types: Record, + visitedNodes = new Set(), + discriminatedUnionVariants = new Set(), + undiscriminatedUnionVariants = new Set(), + depth: number = 0 + ): AlgoliaSearchRecord[] { + if (depth >= 8) { + return []; } - private collectReferencedDiscriminatedUnionToContentV2( - discriminatedUnion: APIV1Read.TypeShape.DiscriminatedUnion, - fields: AlgoliaSearchRecord[], - typeReferenceWithMetadata: TypeReferenceWithMetadata, - baseSlug: string, - additionalProperties: any, - types: Record, - visitedNodes: Set, - discriminatedUnionVariants: Set, - undiscriminatedUnionVariants: Set, - depth: number, - ) { - const referenceLeaves: TypeReferenceWithMetadata[] = []; - const newDiscriminatedUnionVariants = new Set(discriminatedUnionVariants); - - discriminatedUnion.variants.forEach((variant) => { - if (!discriminatedUnionVariants.has(variant.discriminantValue)) { - newDiscriminatedUnionVariants.add(variant.discriminantValue); - const title = variant.discriminantValue ?? encodeURI(variant.displayName ?? ""); - const slug = FernNavigation.V1.Slug(`${baseSlug}.${title}`); - - // additional properties on the variant are the object shapes themselves, - // so we check for extension types here. - variant.additionalProperties.extends.forEach((extend) => { - referenceLeaves.push({ - reference: { type: "id", value: extend, default: undefined }, - anchorIdParts: typeReferenceWithMetadata.anchorIdParts, - breadcrumbs: [...typeReferenceWithMetadata.breadcrumbs, { title, slug }], - slugPrefix: `${baseSlug}.${title}`, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: undefined, - type: typeReferenceWithMetadata.type, + const fields: AlgoliaSearchRecord[] = []; + + typeReferencesWithMetadata.forEach((typeReferenceWithMetadata) => { + // Based on the type of endpoint, we may have additional properties to add to the field, + // this is the tersest way to add them. + const endpointProperties = { + type: "endpoint-field-v1" as const, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + isResponseStream: typeReferenceWithMetadata.isResponseStream, + }; + const websocketProperties = { + type: "websocket-field-v1" as const, + endpointPath: typeReferenceWithMetadata.endpointPath, + }; + const webhookProperties = { + type: "webhook-field-v1" as const, + method: typeReferenceWithMetadata.method, + endpointPath: typeReferenceWithMetadata.endpointPath, + }; + // Done for appropriate type checking + const additionalProperties = + typeReferenceWithMetadata.method != null + ? typeReferenceWithMetadata.type === "endpoint-field-v1" + ? endpointProperties + : webhookProperties + : websocketProperties; + + const baseSlug = typeReferenceWithMetadata.slugPrefix; + + visitDiscriminatedUnion(typeReferenceWithMetadata.reference)._visit({ + id: (id) => { + if (!visitedNodes.has(id.value)) { + const type = types[id.value]; + if (type != null) { + visitDiscriminatedUnion(type.shape)._visit({ + object: (object) => { + this.collectReferencedTypeReferenceToContentV2( + id, + object, + fields, + typeReferenceWithMetadata, + baseSlug, + additionalProperties, + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + ); + }, + alias: () => undefined, + enum: (enum_) => { + enum_.values.forEach((value) => { + const slug = FernNavigation.V1.Slug(baseSlug); + // For enums, we want to make a record for each enum value, but individual enum values + // do not have deep linked anchors. + fields.push({ + objectID: uuid(), + title: value.value, + availability: value.availability, + description: value.description, + breadcrumbs: [ + ...typeReferenceWithMetadata.breadcrumbs, + { + title: value.value, + slug, + }, + ], + slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + extends: undefined, + ...additionalProperties, }); - }); - variant.additionalProperties.properties.forEach((property) => { - // Here we check for references or container types to process in the next iteration. - switch (property.valueType.type) { - case "id": - referenceLeaves.push({ - reference: property.valueType, - anchorIdParts: [...typeReferenceWithMetadata.anchorIdParts, title, property.key], - breadcrumbs: [ - ...typeReferenceWithMetadata.breadcrumbs, - { title, slug }, - { - title: property.key, - slug: `${baseSlug}.${title}.${encodeURI(property.key)}`, - }, - ], - slugPrefix: `${baseSlug}.${title}.${encodeURI(property.key)}`, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: property.key, - type: typeReferenceWithMetadata.type, - }); - break; - // here we check for container types to process in the next iteration - case "optional": - case "map": - case "list": - case "set": - referenceLeaves.push({ - reference: property.valueType, - anchorIdParts: [...typeReferenceWithMetadata.anchorIdParts, title], - breadcrumbs: [...typeReferenceWithMetadata.breadcrumbs, { title, slug }], - slugPrefix: `${baseSlug}.${title}`, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - propertyKey: property.key, - type: typeReferenceWithMetadata.type, - }); - break; - // otherwise we check for primitive or literal types to add to our collection of algolia records. - case "primitive": - case "literal": - case "unknown": - fields.push({ - objectID: uuid(), - title: property.key, - description: property.description, - availability: property.availability, - breadcrumbs: typeReferenceWithMetadata.breadcrumbs.concat({ - title: property.key, - slug, - }), - slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - extends: variant.additionalProperties.extends, - ...additionalProperties, - }); - break; - default: - assertNever(property.valueType); - } - }); - } - }); - - fields.push( - ...this.collectReferencedTypesToContentV2( - referenceLeaves, - types, - visitedNodes, - newDiscriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); - } - - // The idea behind this function is to collect the smallest subset of records that capture all type information. - // - // To do this, we descend the type tree and resolve the leaf nodes and map them to records that have built up breadcrumbs, - // slugs, and other metadata that is useful for search indexing. - protected collectReferencedTypesToContentV2( - typeReferencesWithMetadata: TypeReferenceWithMetadata[], - types: Record, - visitedNodes = new Set(), - discriminatedUnionVariants = new Set(), - undiscriminatedUnionVariants = new Set(), - depth: number = 0, - ): AlgoliaSearchRecord[] { - if (depth >= 8) { - return []; - } - - const fields: AlgoliaSearchRecord[] = []; - - typeReferencesWithMetadata.forEach((typeReferenceWithMetadata) => { - // Based on the type of endpoint, we may have additional properties to add to the field, - // this is the tersest way to add them. - const endpointProperties = { - type: "endpoint-field-v1" as const, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - isResponseStream: typeReferenceWithMetadata.isResponseStream, - }; - const websocketProperties = { - type: "websocket-field-v1" as const, - endpointPath: typeReferenceWithMetadata.endpointPath, - }; - const webhookProperties = { - type: "webhook-field-v1" as const, - method: typeReferenceWithMetadata.method, - endpointPath: typeReferenceWithMetadata.endpointPath, - }; - // Done for appropriate type checking - const additionalProperties = - typeReferenceWithMetadata.method != null - ? typeReferenceWithMetadata.type === "endpoint-field-v1" - ? endpointProperties - : webhookProperties - : websocketProperties; - - const baseSlug = typeReferenceWithMetadata.slugPrefix; - - visitDiscriminatedUnion(typeReferenceWithMetadata.reference)._visit({ - id: (id) => { - if (!visitedNodes.has(id.value)) { - const type = types[id.value]; - if (type != null) { - visitDiscriminatedUnion(type.shape)._visit({ - object: (object) => { - this.collectReferencedTypeReferenceToContentV2( - id, - object, - fields, - typeReferenceWithMetadata, - baseSlug, - additionalProperties, - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth, - ); - }, - alias: () => undefined, - enum: (enum_) => { - enum_.values.forEach((value) => { - const slug = FernNavigation.V1.Slug(baseSlug); - // For enums, we want to make a record for each enum value, but individual enum values - // do not have deep linked anchors. - fields.push({ - objectID: uuid(), - title: value.value, - availability: value.availability, - description: value.description, - breadcrumbs: [ - ...typeReferenceWithMetadata.breadcrumbs, - { - title: value.value, - slug, - }, - ], - slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - extends: undefined, - ...additionalProperties, - }); - }); - }, - undiscriminatedUnion: (undiscriminatedUnion) => { - this.collectReferencedUndiscriminatedUnionToContentV2( - undiscriminatedUnion, - fields, - typeReferenceWithMetadata, - baseSlug, - additionalProperties, - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth, - ); - }, - discriminatedUnion: (discriminatedUnion) => { - this.collectReferencedDiscriminatedUnionToContentV2( - discriminatedUnion, - fields, - typeReferenceWithMetadata, - baseSlug, - additionalProperties, - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth, - ); - }, - }); - } - } else { - // In this case, we check to see if we've already visited the reference, we do not want to process it again. - // We treat this as a leaf node, and if the object came from a propert, we add the record as being keyed by the parent key. - if (typeReferenceWithMetadata.propertyKey != null) { - const type = types[id.value]; - - if (type) { - const slug = FernNavigation.V1.Slug( - `${baseSlug}.${encodeURI(typeReferenceWithMetadata.propertyKey)}`, - ); - fields.push({ - objectID: uuid(), - title: typeReferenceWithMetadata.propertyKey, - description: type.description, - availability: type.availability, - breadcrumbs: typeReferenceWithMetadata.breadcrumbs.concat({ - title: typeReferenceWithMetadata.propertyKey, - slug, - }), - slug, - version: typeReferenceWithMetadata.version, - indexSegmentId: typeReferenceWithMetadata.indexSegmentId, - extends: type.shape.type === "object" ? type.shape.extends : undefined, - ...additionalProperties, - }); - } - } - } + }); }, - optional: (optional) => { - // Here, we want to unwrap the container type while preserving the parent breadcrumbs. - fields.push( - ...this.collectReferencedTypesToContentV2( - [ - { - ...typeReferenceWithMetadata, - reference: optional.itemType, - }, - ], - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); + undiscriminatedUnion: (undiscriminatedUnion) => { + this.collectReferencedUndiscriminatedUnionToContentV2( + undiscriminatedUnion, + fields, + typeReferenceWithMetadata, + baseSlug, + additionalProperties, + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + ); }, - list: (list) => { - // Append index here - // Here, we want to unwrap the container type while preserving the parent breadcrumbs - fields.push( - ...this.collectReferencedTypesToContentV2( - [ - { - ...typeReferenceWithMetadata, - reference: list.itemType, - }, - ], - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); + discriminatedUnion: (discriminatedUnion) => { + this.collectReferencedDiscriminatedUnionToContentV2( + discriminatedUnion, + fields, + typeReferenceWithMetadata, + baseSlug, + additionalProperties, + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + ); }, - set: (set) => { - // Here, we want to unwrap the container type while preserving the parent breadcrumbs - fields.push( - ...this.collectReferencedTypesToContentV2( - [ - { - ...typeReferenceWithMetadata, - reference: set.itemType, - }, - ], - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); + }); + } + } else { + // In this case, we check to see if we've already visited the reference, we do not want to process it again. + // We treat this as a leaf node, and if the object came from a propert, we add the record as being keyed by the parent key. + if (typeReferenceWithMetadata.propertyKey != null) { + const type = types[id.value]; + + if (type) { + const slug = FernNavigation.V1.Slug( + `${baseSlug}.${encodeURI(typeReferenceWithMetadata.propertyKey)}` + ); + fields.push({ + objectID: uuid(), + title: typeReferenceWithMetadata.propertyKey, + description: type.description, + availability: type.availability, + breadcrumbs: typeReferenceWithMetadata.breadcrumbs.concat({ + title: typeReferenceWithMetadata.propertyKey, + slug, + }), + slug, + version: typeReferenceWithMetadata.version, + indexSegmentId: typeReferenceWithMetadata.indexSegmentId, + extends: + type.shape.type === "object" + ? type.shape.extends + : undefined, + ...additionalProperties, + }); + } + } + } + }, + optional: (optional) => { + // Here, we want to unwrap the container type while preserving the parent breadcrumbs. + fields.push( + ...this.collectReferencedTypesToContentV2( + [ + { + ...typeReferenceWithMetadata, + reference: optional.itemType, }, - map: (map) => { - // Here, we want to unwrap the container type while preserving the parent breadcrumbs - fields.push( - ...this.collectReferencedTypesToContentV2( - [ - { - ...typeReferenceWithMetadata, - reference: map.valueType, - }, - ], - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); - fields.push( - ...this.collectReferencedTypesToContentV2( - [ - { - ...typeReferenceWithMetadata, - reference: map.valueType, - }, - ], - types, - visitedNodes, - discriminatedUnionVariants, - undiscriminatedUnionVariants, - depth + 1, - ), - ); + ], + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + }, + list: (list) => { + // Append index here + // Here, we want to unwrap the container type while preserving the parent breadcrumbs + fields.push( + ...this.collectReferencedTypesToContentV2( + [ + { + ...typeReferenceWithMetadata, + reference: list.itemType, }, - primitive: () => { - return; + ], + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + }, + set: (set) => { + // Here, we want to unwrap the container type while preserving the parent breadcrumbs + fields.push( + ...this.collectReferencedTypesToContentV2( + [ + { + ...typeReferenceWithMetadata, + reference: set.itemType, }, - literal: () => { - return; + ], + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + }, + map: (map) => { + // Here, we want to unwrap the container type while preserving the parent breadcrumbs + fields.push( + ...this.collectReferencedTypesToContentV2( + [ + { + ...typeReferenceWithMetadata, + reference: map.valueType, }, - unknown: () => { - return; + ], + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + fields.push( + ...this.collectReferencedTypesToContentV2( + [ + { + ...typeReferenceWithMetadata, + reference: map.valueType, }, - }); - }); - - return fields; - } - - protected generateAlgoliaSearchRecordsForChangelogNodeV2( - root: FernNavigation.V1.ChangelogNode, - context: NavigationContext, - ): AlgoliaSearchRecord[] { - const records: AlgoliaSearchRecord[] = []; - - const breadcrumbs = context.pathParts.map((part) => ({ - title: part.name, - slug: part.urlSlug, - })); - - FernNavigation.V1.traverseDF(root, (node) => { - if (!FernNavigation.V1.hasMetadata(node)) { - return; - } - - if (node.hidden) { - return "skip"; - } - - if (FernNavigation.V1.hasMarkdown(node)) { - const pageId = FernNavigation.V1.getPageId(node); - if (pageId == null) { - return; - } - - const md = this.config.docsDefinition.pages[pageId]?.markdown; - if (md == null) { - LOGGER.error("Failed to find markdown for node", node); - return; - } + ], + types, + visitedNodes, + discriminatedUnionVariants, + undiscriminatedUnionVariants, + depth + 1 + ) + ); + }, + primitive: () => { + return; + }, + literal: () => { + return; + }, + unknown: () => { + return; + }, + }); + }); + + return fields; + } + + protected generateAlgoliaSearchRecordsForChangelogNodeV2( + root: FernNavigation.V1.ChangelogNode, + context: NavigationContext + ): AlgoliaSearchRecord[] { + const records: AlgoliaSearchRecord[] = []; + + const breadcrumbs = context.pathParts.map((part) => ({ + title: part.name, + slug: part.urlSlug, + })); + + FernNavigation.V1.traverseDF(root, (node) => { + if (!FernNavigation.V1.hasMetadata(node)) { + return; + } + + if (node.hidden) { + return "skip"; + } + + if (FernNavigation.V1.hasMarkdown(node)) { + const pageId = FernNavigation.V1.getPageId(node); + if (pageId == null) { + return; + } - const markdownSectionRecords = this.parseMarkdownItem( - md, - breadcrumbs, - context.indexSegment, - node.slug, - node.title, - ); + const md = this.config.docsDefinition.pages[pageId]?.markdown; + if (md == null) { + LOGGER.error("Failed to find markdown for node", node); + return; + } - records.push(...markdownSectionRecords); - } - return; - }); + const markdownSectionRecords = this.parseMarkdownItem( + md, + breadcrumbs, + context.indexSegment, + node.slug, + node.title + ); - return records; + records.push(...markdownSectionRecords); + } + return; + }); + + return records; + } + + protected generateAlgoliaSearchRecordsForChangelogSectionV2( + changelog: DocsV1Read.ChangelogSection, + context: NavigationContext, + fallbackTitle: string = "Changelog" + ): AlgoliaSearchRecord[] { + if (changelog.hidden) { + return []; } + const records: AlgoliaSearchRecord[] = []; + if (changelog.pageId != null) { + const changelogPageContent = + this.config.docsDefinition.pages[changelog.pageId]; + const slug = FernNavigation.V1.Slug(changelog.urlSlug); + const title = changelog.title ?? fallbackTitle; + + if (changelogPageContent != null) { + const markdownSectionRecords = this.parseMarkdownItem( + changelogPageContent.markdown, + [], + context.indexSegment, + slug, + title + ); - protected generateAlgoliaSearchRecordsForChangelogSectionV2( - changelog: DocsV1Read.ChangelogSection, - context: NavigationContext, - fallbackTitle: string = "Changelog", - ): AlgoliaSearchRecord[] { - if (changelog.hidden) { - return []; - } - const records: AlgoliaSearchRecord[] = []; - if (changelog.pageId != null) { - const changelogPageContent = this.config.docsDefinition.pages[changelog.pageId]; - const slug = FernNavigation.V1.Slug(changelog.urlSlug); - const title = changelog.title ?? fallbackTitle; - - if (changelogPageContent != null) { - const markdownSectionRecords = this.parseMarkdownItem( - changelogPageContent.markdown, - [], - context.indexSegment, - slug, - title, - ); - - records.push(...markdownSectionRecords); - } - - changelog.items.forEach((changelogItem) => { - const changelogTitle = `${title} - ${changelogItem.date}`; - const slug = FernNavigation.V1.Slug(changelog.urlSlug); - const changelogPageContent = this.config.docsDefinition.pages[changelogItem.pageId]; - if (changelogPageContent != null) { - const markdownSectionRecords = this.parseMarkdownItem( - changelogPageContent.markdown, - [], - context.indexSegment, - slug, - changelogTitle, - ); + records.push(...markdownSectionRecords); + } + + changelog.items.forEach((changelogItem) => { + const changelogTitle = `${title} - ${changelogItem.date}`; + const slug = FernNavigation.V1.Slug(changelog.urlSlug); + const changelogPageContent = + this.config.docsDefinition.pages[changelogItem.pageId]; + if (changelogPageContent != null) { + const markdownSectionRecords = this.parseMarkdownItem( + changelogPageContent.markdown, + [], + context.indexSegment, + slug, + changelogTitle + ); - records.push(...markdownSectionRecords); - } - }); + records.push(...markdownSectionRecords); } - - return records; + }); } + + return records; + } } function toBreadcrumbs( - breadcrumbs: { - title: string; - slug: string; - }[], - parents: readonly FernNavigation.V1.NavigationNode[], + breadcrumbs: { + title: string; + slug: string; + }[], + parents: readonly FernNavigation.V1.NavigationNode[] ): BreadcrumbsInfo[] { - return [ - ...breadcrumbs, - ...parents - .filter(FernNavigation.V1.hasMetadata) - .filter((parent) => - parent.type === "apiReference" - ? !parent.hideTitle - : parent.type === "changelogMonth" || parent.type === "changelogYear" - ? false - : true, - ) - .map((parent) => ({ - title: parent.title, - slug: parent.slug, - })), - ]; + return [ + ...breadcrumbs, + ...parents + .filter(FernNavigation.V1.hasMetadata) + .filter((parent) => + parent.type === "apiReference" + ? !parent.hideTitle + : parent.type === "changelogMonth" || parent.type === "changelogYear" + ? false + : true + ) + .map((parent) => ({ + title: parent.title, + slug: parent.slug, + })), + ]; } function anchorIdToSlug( - node: FernNavigation.V1.EndpointNode | FernNavigation.V1.WebSocketNode | FernNavigation.V1.WebhookNode, - anchorIdParts: string[], + node: + | FernNavigation.V1.EndpointNode + | FernNavigation.V1.WebSocketNode + | FernNavigation.V1.WebhookNode, + anchorIdParts: string[] ): FernNavigation.V1.Slug { - return FernNavigation.V1.Slug(`${node.slug}#${encodeURI(anchorIdParts.join("."))}`); + return FernNavigation.V1.Slug( + `${node.slug}#${encodeURI(anchorIdParts.join("."))}` + ); } -export function getMarkdownSectionTree(markdown: string, pageTitle: string): MarkdownNode { - const { frontmatter, content } = getFrontmatter(markdown); - const lines: string[] = content.split("\n"); - let insideCodeBlock = false; - const root: MarkdownNode = { level: 0, heading: frontmatter.title ?? pageTitle, content: "", children: [] }; - const collectedNodes = [root]; - - for (const line of lines) { - const trimmedLine = line.trim(); - - if (trimmedLine.startsWith("```") || trimmedLine.startsWith("~~~")) { - insideCodeBlock = !insideCodeBlock; - } - - let currentNode = collectedNodes.pop(); - if (!insideCodeBlock && trimmedLine.startsWith("#")) { - const headerMatch = trimmedLine.match(/^(#{1,6})\s+(.*)$/); - if (headerMatch) { - const level = headerMatch[1]?.length; - const heading = headerMatch[2]?.trim(); - if (currentNode != null && level != null && heading != null) { - while (currentNode != null && currentNode.level >= level) { - currentNode = collectedNodes.pop(); - } - const newNode = { level, heading, content: "", children: [] }; - if (currentNode != null && currentNode.level < level) { - currentNode.children.push(newNode); - } - - if (currentNode) { - collectedNodes.push(currentNode); - collectedNodes.push(newNode); - } - continue; - } - } - } +export function getMarkdownSectionTree( + markdown: string, + pageTitle: string +): MarkdownNode { + const { frontmatter, content } = getFrontmatter(markdown); + const lines: string[] = content.split("\n"); + let insideCodeBlock = false; + const root: MarkdownNode = { + level: 0, + heading: frontmatter.title ?? pageTitle, + content: "", + children: [], + }; + const collectedNodes = [root]; + + for (const line of lines) { + const trimmedLine = line.trim(); + + if (trimmedLine.startsWith("```") || trimmedLine.startsWith("~~~")) { + insideCodeBlock = !insideCodeBlock; + } - if (currentNode) { - currentNode.content += trimmedLine + "\n"; + let currentNode = collectedNodes.pop(); + if (!insideCodeBlock && trimmedLine.startsWith("#")) { + const headerMatch = trimmedLine.match(/^(#{1,6})\s+(.*)$/); + if (headerMatch) { + const level = headerMatch[1]?.length; + const heading = headerMatch[2]?.trim(); + if (currentNode != null && level != null && heading != null) { + while (currentNode != null && currentNode.level >= level) { + currentNode = collectedNodes.pop(); + } + const newNode = { level, heading, content: "", children: [] }; + if (currentNode != null && currentNode.level < level) { + currentNode.children.push(newNode); + } + + if (currentNode) { collectedNodes.push(currentNode); + collectedNodes.push(newNode); + } + continue; } + } } - return root; + if (currentNode) { + currentNode.content += trimmedLine + "\n"; + collectedNodes.push(currentNode); + } + } + + return root; } function sanitizeText(text: string): string { - return text.replace(/<[^>]*>/g, "").replace(/&[^;]+;/g, ""); + return text.replace(/<[^>]*>/g, "").replace(/&[^;]+;/g, ""); } export function getMarkdownSections( - markdownSection: MarkdownNode, - breadcrumbs: BreadcrumbsInfo[], - indexSegmentId: FdrAPI.IndexSegmentId, - slug: FernNavigation.V1.Slug, + markdownSection: MarkdownNode, + breadcrumbs: BreadcrumbsInfo[], + indexSegmentId: FdrAPI.IndexSegmentId, + slug: FernNavigation.V1.Slug ): AlgoliaSearchRecord[] { - const markdownSlug = FernNavigation.V1.Slug( - markdownSection.level === 0 ? slug : `${slug}#${encodeURI(kebabCase(markdownSection.heading.toLowerCase()))}`, - ); - const sectionBreadcrumbs = markdownSection.heading - ? breadcrumbs.concat([ - { - title: markdownSection.heading, - slug: markdownSlug, - }, - ]) - : breadcrumbs.slice(0); - - const records: AlgoliaSearchRecord[] = - markdownSection.content.trim().length === 0 - ? [] - : [ - compact({ - type: "markdown-section-v1", - objectID: uuid(), - title: markdownSection.heading.trim(), - content: truncateToBytes(sanitizeText(markdownSection.content.trim()), 50 * 1000), - breadcrumbs: sectionBreadcrumbs, - indexSegmentId, - slug: markdownSlug, - description: undefined, - version: undefined, - }), - ]; - return records.concat( - markdownSection.children.reduce((acc: AlgoliaSearchRecord[], markdownSectionChild: MarkdownNode) => { - return acc.concat(getMarkdownSections(markdownSectionChild, sectionBreadcrumbs, indexSegmentId, slug)); - }, []), - ); + const markdownSlug = FernNavigation.V1.Slug( + markdownSection.level === 0 + ? slug + : `${slug}#${encodeURI(kebabCase(markdownSection.heading.toLowerCase()))}` + ); + const sectionBreadcrumbs = markdownSection.heading + ? breadcrumbs.concat([ + { + title: markdownSection.heading, + slug: markdownSlug, + }, + ]) + : breadcrumbs.slice(0); + + const records: AlgoliaSearchRecord[] = + markdownSection.content.trim().length === 0 + ? [] + : [ + compact({ + type: "markdown-section-v1", + objectID: uuid(), + title: markdownSection.heading.trim(), + content: truncateToBytes( + sanitizeText(markdownSection.content.trim()), + 50 * 1000 + ), + breadcrumbs: sectionBreadcrumbs, + indexSegmentId, + slug: markdownSlug, + description: undefined, + version: undefined, + }), + ]; + return records.concat( + markdownSection.children.reduce( + (acc: AlgoliaSearchRecord[], markdownSectionChild: MarkdownNode) => { + return acc.concat( + getMarkdownSections( + markdownSectionChild, + sectionBreadcrumbs, + indexSegmentId, + slug + ) + ); + }, + [] + ) + ); } diff --git a/servers/fdr/src/services/algolia/AlgoliaService.ts b/servers/fdr/src/services/algolia/AlgoliaService.ts index a4685b3f88..fe803238d7 100644 --- a/servers/fdr/src/services/algolia/AlgoliaService.ts +++ b/servers/fdr/src/services/algolia/AlgoliaService.ts @@ -6,72 +6,79 @@ import { AlgoliaSearchRecordGeneratorV2 } from "./AlgoliaSearchRecordGeneratorV2 import type { AlgoliaSearchRecord, ConfigSegmentTuple } from "./types"; export interface AlgoliaService { - deleteIndexSegmentRecords(indexSegmentIds: string[]): Promise; + deleteIndexSegmentRecords(indexSegmentIds: string[]): Promise; - generateSearchRecords(params: { - url: string; - docsDefinition: DocsV1Db.DocsDefinitionDb; - apiDefinitionsById: Record; - configSegmentTuples: ConfigSegmentTuple[]; - }): Promise; + generateSearchRecords(params: { + url: string; + docsDefinition: DocsV1Db.DocsDefinitionDb; + apiDefinitionsById: Record; + configSegmentTuples: ConfigSegmentTuple[]; + }): Promise; - uploadSearchRecords(records: AlgoliaSearchRecord[]): Promise; + uploadSearchRecords(records: AlgoliaSearchRecord[]): Promise; - generateSearchApiKey(filters: string, validUntil: Date): string; + generateSearchApiKey(filters: string, validUntil: Date): string; } export class AlgoliaServiceImpl implements AlgoliaService { - private readonly client: SearchClient; + private readonly client: SearchClient; - private get baseSearchApiKey() { - return this.app.config.algoliaSearchApiKey; - } + private get baseSearchApiKey() { + return this.app.config.algoliaSearchApiKey; + } - private get index() { - return this.client.initIndex(this.app.config.algoliaSearchIndex); - } + private get index() { + return this.client.initIndex(this.app.config.algoliaSearchIndex); + } - public constructor(private readonly app: FdrApplication) { - const { config } = app; - this.client = algolia(config.algoliaAppId, config.algoliaAdminApiKey); - } + public constructor(private readonly app: FdrApplication) { + const { config } = app; + this.client = algolia(config.algoliaAppId, config.algoliaAdminApiKey); + } - public async uploadSearchRecords(records: AlgoliaSearchRecord[]) { - await this.index.saveObjects(records).wait(); - } + public async uploadSearchRecords(records: AlgoliaSearchRecord[]) { + await this.index.saveObjects(records).wait(); + } - public async generateSearchRecords({ - url, - docsDefinition, - apiDefinitionsById, - configSegmentTuples, - }: { - url: string; - docsDefinition: DocsV1Db.DocsDefinitionDb; - apiDefinitionsById: Record; - configSegmentTuples: ConfigSegmentTuple[]; - }) { - return configSegmentTuples.flatMap(([config, indexSegment]) => { - const generator = new ( - url.includes("workato") ? AlgoliaSearchRecordGeneratorV2 : AlgoliaSearchRecordGenerator - )({ docsDefinition, apiDefinitionsById }); + public async generateSearchRecords({ + url, + docsDefinition, + apiDefinitionsById, + configSegmentTuples, + }: { + url: string; + docsDefinition: DocsV1Db.DocsDefinitionDb; + apiDefinitionsById: Record; + configSegmentTuples: ConfigSegmentTuple[]; + }) { + return configSegmentTuples.flatMap(([config, indexSegment]) => { + const generator = new ( + url.includes("workato") + ? AlgoliaSearchRecordGeneratorV2 + : AlgoliaSearchRecordGenerator + )({ docsDefinition, apiDefinitionsById }); - if (config == null) { - return []; - } - return generator.generateAlgoliaSearchRecordsForSpecificDocsVersion(config, indexSegment); - }); - } + if (config == null) { + return []; + } + return generator.generateAlgoliaSearchRecordsForSpecificDocsVersion( + config, + indexSegment + ); + }); + } - public generateSearchApiKey(filters: string, validUntil: Date) { - return this.client.generateSecuredApiKey(this.baseSearchApiKey, { - filters, - validUntil: Math.floor(validUntil.getTime() / 1_000), - }); - } + public generateSearchApiKey(filters: string, validUntil: Date) { + return this.client.generateSecuredApiKey(this.baseSearchApiKey, { + filters, + validUntil: Math.floor(validUntil.getTime() / 1_000), + }); + } - public async deleteIndexSegmentRecords(indexSegmentIds: string[]) { - const filters = indexSegmentIds.map((indexSegmentId) => `indexSegmentId:${indexSegmentId}`).join(" OR "); - await this.index.deleteBy({ filters }).wait(); - } + public async deleteIndexSegmentRecords(indexSegmentIds: string[]) { + const filters = indexSegmentIds + .map((indexSegmentId) => `indexSegmentId:${indexSegmentId}`) + .join(" OR "); + await this.index.deleteBy({ filters }).wait(); + } } diff --git a/servers/fdr/src/services/algolia/NavigationContext.ts b/servers/fdr/src/services/algolia/NavigationContext.ts index ffe06dad34..0862ff4700 100644 --- a/servers/fdr/src/services/algolia/NavigationContext.ts +++ b/servers/fdr/src/services/algolia/NavigationContext.ts @@ -2,65 +2,73 @@ import { Algolia } from "@fern-api/fdr-sdk"; import type { IndexSegment } from "./types"; export class NavigationContext { - #indexSegment: IndexSegment; - #pathParts: Algolia.AlgoliaRecordPathPart[]; + #indexSegment: IndexSegment; + #pathParts: Algolia.AlgoliaRecordPathPart[]; - /** - * The path represented by context slugs. - */ - public get path() { - return this.#pathParts - .filter((p) => p.skipUrlSlug == null || !p.skipUrlSlug) - .map((p) => p.urlSlug) - .join("/"); - } + /** + * The path represented by context slugs. + */ + public get path() { + return this.#pathParts + .filter((p) => p.skipUrlSlug == null || !p.skipUrlSlug) + .map((p) => p.urlSlug) + .join("/"); + } - /** - * The path represented by context slugs. - */ - public get pathParts() { - return [...this.#pathParts]; - } + /** + * The path represented by context slugs. + */ + public get pathParts() { + return [...this.#pathParts]; + } - public constructor( - public readonly indexSegment: IndexSegment, - pathParts: Algolia.AlgoliaRecordPathPart[], - ) { - this.#indexSegment = indexSegment; - this.#pathParts = pathParts; - } + public constructor( + public readonly indexSegment: IndexSegment, + pathParts: Algolia.AlgoliaRecordPathPart[] + ) { + this.#indexSegment = indexSegment; + this.#pathParts = pathParts; + } - /** - * @returns A new `NavigationContext` instance. - */ - public withPathPart(pathPart: Algolia.AlgoliaRecordPathPart) { - return this.withPathParts([pathPart]); - } + /** + * @returns A new `NavigationContext` instance. + */ + public withPathPart(pathPart: Algolia.AlgoliaRecordPathPart) { + return this.withPathParts([pathPart]); + } - /** - * @returns A new `NavigationContext` instance. - */ - public withPathParts(pathParts: Algolia.AlgoliaRecordPathPart[]) { - return new NavigationContext(this.#indexSegment, [...this.#pathParts, ...pathParts]); - } + /** + * @returns A new `NavigationContext` instance. + */ + public withPathParts(pathParts: Algolia.AlgoliaRecordPathPart[]) { + return new NavigationContext(this.#indexSegment, [ + ...this.#pathParts, + ...pathParts, + ]); + } - /** - * @returns A new `NavigationContext` instance. - */ - public withFullSlug(fullSlug: string[]) { - // we check if the full slug starts with the version, to see if there would be duplicate versions in the slug - // as opposed to filtering out all (which would become chaotic if deeply in the slug) - // this is a patch fix, since we don't know where full slug is coming from. If more bugs are encountered, - // look into fdr to see where fullSlug comes from - const { indexSegment } = this; - const slug = - fullSlug[0] === (indexSegment.type === "versioned" && indexSegment.version.urlSlug) - ? fullSlug.slice(1) - : fullSlug; + /** + * @returns A new `NavigationContext` instance. + */ + public withFullSlug(fullSlug: string[]) { + // we check if the full slug starts with the version, to see if there would be duplicate versions in the slug + // as opposed to filtering out all (which would become chaotic if deeply in the slug) + // this is a patch fix, since we don't know where full slug is coming from. If more bugs are encountered, + // look into fdr to see where fullSlug comes from + const { indexSegment } = this; + const slug = + fullSlug[0] === + (indexSegment.type === "versioned" && indexSegment.version.urlSlug) + ? fullSlug.slice(1) + : fullSlug; - return new NavigationContext( - this.#indexSegment, - slug.map((urlSlug) => ({ name: urlSlug, urlSlug, skipUrlSlug: undefined })), - ); - } + return new NavigationContext( + this.#indexSegment, + slug.map((urlSlug) => ({ + name: urlSlug, + urlSlug, + skipUrlSlug: undefined, + })) + ); + } } diff --git a/servers/fdr/src/services/algolia/getAllReferencedTypes.ts b/servers/fdr/src/services/algolia/getAllReferencedTypes.ts index 505b7761cf..ad54c3403b 100644 --- a/servers/fdr/src/services/algolia/getAllReferencedTypes.ts +++ b/servers/fdr/src/services/algolia/getAllReferencedTypes.ts @@ -1,137 +1,170 @@ import { APIV1Read } from "@fern-api/fdr-sdk"; import { assertNever } from "../../util"; -export type ReferencedTypes = Record; +export type ReferencedTypes = Record< + APIV1Read.TypeId, + APIV1Read.TypeDefinition +>; export function getAllReferencedTypes({ - reference, - types, + reference, + types, }: { - reference: APIV1Read.TypeReference; - types: Record; + reference: APIV1Read.TypeReference; + types: Record; }): ReferencedTypes { - const visitedTypes = new Set(); - function getAllReferencedTypesFromReference({ - reference, - results, - }: { - reference: APIV1Read.TypeReference; - results: ReferencedTypes; - }): ReferencedTypes { - switch (reference.type) { - case "id": { - if (visitedTypes.has(reference.value)) { - break; - } else { - visitedTypes.add(reference.value); - } - const type = types[reference.value]; - if (type != null) { - return { - ...results, - [reference.value]: type, - ...getAllReferencedTypesFromDefinition({ definition: type, results }), - }; - } - break; - } - case "map": - return { - ...results, - ...getAllReferencedTypesFromReference({ reference: reference.keyType, results }), - ...getAllReferencedTypesFromReference({ reference: reference.valueType, results }), - }; - case "list": - return { - ...results, - ...getAllReferencedTypesFromReference({ reference: reference.itemType, results }), - }; - case "set": - return { - ...results, - ...getAllReferencedTypesFromReference({ reference: reference.itemType, results }), - }; - case "optional": - return { - ...results, - ...getAllReferencedTypesFromReference({ reference: reference.itemType, results }), - }; - case "literal": - case "primitive": - case "unknown": - return results; - default: - assertNever(reference); + const visitedTypes = new Set(); + function getAllReferencedTypesFromReference({ + reference, + results, + }: { + reference: APIV1Read.TypeReference; + results: ReferencedTypes; + }): ReferencedTypes { + switch (reference.type) { + case "id": { + if (visitedTypes.has(reference.value)) { + break; + } else { + visitedTypes.add(reference.value); } + const type = types[reference.value]; + if (type != null) { + return { + ...results, + [reference.value]: type, + ...getAllReferencedTypesFromDefinition({ + definition: type, + results, + }), + }; + } + break; + } + case "map": + return { + ...results, + ...getAllReferencedTypesFromReference({ + reference: reference.keyType, + results, + }), + ...getAllReferencedTypesFromReference({ + reference: reference.valueType, + results, + }), + }; + case "list": + return { + ...results, + ...getAllReferencedTypesFromReference({ + reference: reference.itemType, + results, + }), + }; + case "set": + return { + ...results, + ...getAllReferencedTypesFromReference({ + reference: reference.itemType, + results, + }), + }; + case "optional": + return { + ...results, + ...getAllReferencedTypesFromReference({ + reference: reference.itemType, + results, + }), + }; + case "literal": + case "primitive": + case "unknown": return results; + default: + assertNever(reference); } + return results; + } - function getAllReferencedTypesFromDefinition({ - definition, - results, - }: { - definition: APIV1Read.TypeDefinition; - results: ReferencedTypes; - }): ReferencedTypes { - switch (definition.shape.type) { - case "object": { - return { - ...results, - ...definition.shape.extends.reduce((base, value) => { - return { - ...base, - ...getAllReferencedTypesFromReference({ - reference: { type: "id", value, default: undefined }, - results, - }), - }; - }, {}), - ...definition.shape.properties.reduce((base, value) => { - return { - ...base, - ...getAllReferencedTypesFromReference({ reference: value.valueType, results }), - }; - }, {}), - }; - } - case "alias": - return results; - case "discriminatedUnion": - return { - ...results, - ...definition.shape.variants.reduce((base, value) => { - return { - ...base, - ...getAllReferencedTypesFromDefinition({ - definition: { - name: value.discriminantValue, - shape: { type: "object", ...value.additionalProperties }, - description: undefined, - availability: undefined, - }, - results, - }), - }; - }, {}), - }; - case "enum": - return results; - case "undiscriminatedUnion": - return { - ...results, - ...definition.shape.variants.reduce((base, value) => { - return { - ...base, - ...getAllReferencedTypesFromReference({ - reference: value.type, - results, - }), - }; - }, {}), - }; - default: - assertNever(definition.shape); - } + function getAllReferencedTypesFromDefinition({ + definition, + results, + }: { + definition: APIV1Read.TypeDefinition; + results: ReferencedTypes; + }): ReferencedTypes { + switch (definition.shape.type) { + case "object": { + return { + ...results, + ...definition.shape.extends.reduce((base, value) => { + return { + ...base, + ...getAllReferencedTypesFromReference({ + reference: { type: "id", value, default: undefined }, + results, + }), + }; + }, {}), + ...definition.shape.properties.reduce( + (base, value) => { + return { + ...base, + ...getAllReferencedTypesFromReference({ + reference: value.valueType, + results, + }), + }; + }, + {} + ), + }; + } + case "alias": + return results; + case "discriminatedUnion": + return { + ...results, + ...definition.shape.variants.reduce( + (base, value) => { + return { + ...base, + ...getAllReferencedTypesFromDefinition({ + definition: { + name: value.discriminantValue, + shape: { type: "object", ...value.additionalProperties }, + description: undefined, + availability: undefined, + }, + results, + }), + }; + }, + {} + ), + }; + case "enum": + return results; + case "undiscriminatedUnion": + return { + ...results, + ...definition.shape.variants.reduce( + (base, value) => { + return { + ...base, + ...getAllReferencedTypesFromReference({ + reference: value.type, + results, + }), + }; + }, + {} + ), + }; + default: + assertNever(definition.shape); } - return getAllReferencedTypesFromReference({ reference, results: {} }); + } + return getAllReferencedTypesFromReference({ reference, results: {} }); } diff --git a/servers/fdr/src/services/algolia/index.ts b/servers/fdr/src/services/algolia/index.ts index 52a9ffbe21..18ec2a3af2 100644 --- a/servers/fdr/src/services/algolia/index.ts +++ b/servers/fdr/src/services/algolia/index.ts @@ -1,2 +1,6 @@ export { AlgoliaServiceImpl, type AlgoliaService } from "./AlgoliaService"; -export type { AlgoliaSearchRecord, ConfigSegmentTuple, IndexSegment } from "./types"; +export type { + AlgoliaSearchRecord, + ConfigSegmentTuple, + IndexSegment, +} from "./types"; diff --git a/servers/fdr/src/services/algolia/types.ts b/servers/fdr/src/services/algolia/types.ts index 84a19a1ea3..e1600f696d 100644 --- a/servers/fdr/src/services/algolia/types.ts +++ b/servers/fdr/src/services/algolia/types.ts @@ -2,64 +2,64 @@ import { APIV1Read, Algolia, DocsV1Db, FdrAPI } from "@fern-api/fdr-sdk"; import type { DocsVersion } from "../../types"; export type IndexSegment = - | { - type: "versioned"; - id: FdrAPI.IndexSegmentId; - searchApiKey: string; - version: DocsVersion; - } - | { - type: "unversioned"; - id: FdrAPI.IndexSegmentId; - searchApiKey: string; - }; + | { + type: "versioned"; + id: FdrAPI.IndexSegmentId; + searchApiKey: string; + version: DocsVersion; + } + | { + type: "unversioned"; + id: FdrAPI.IndexSegmentId; + searchApiKey: string; + }; export type ConfigSegmentTuple = readonly [ - config: DocsV1Db.UnversionedNavigationConfig | undefined, - segment: IndexSegment, + config: DocsV1Db.UnversionedNavigationConfig | undefined, + segment: IndexSegment, ]; type WithObjectId = { - [K: string]: unknown; - objectID: string; + [K: string]: unknown; + objectID: string; } & T; export type AlgoliaSearchRecord = WithObjectId; export type TypeReferenceWithMetadata = { - reference: APIV1Read.TypeReference; - anchorIdParts: string[]; - breadcrumbs: Algolia.BreadcrumbsInfo[]; - slugPrefix: string; - version: Algolia.AlgoliaRecordVersionV3 | undefined; - indexSegmentId: FdrAPI.IndexSegmentId; - method: FdrAPI.HttpMethod; // websocket is always GET (during handshake) - endpointPath: APIV1Read.EndpointPathPart[]; - isResponseStream?: boolean; - propertyKey: string | undefined; - type: "endpoint-field-v1" | "websocket-field-v1" | "webhook-field-v1"; + reference: APIV1Read.TypeReference; + anchorIdParts: string[]; + breadcrumbs: Algolia.BreadcrumbsInfo[]; + slugPrefix: string; + version: Algolia.AlgoliaRecordVersionV3 | undefined; + indexSegmentId: FdrAPI.IndexSegmentId; + method: FdrAPI.HttpMethod; // websocket is always GET (during handshake) + endpointPath: APIV1Read.EndpointPathPart[]; + isResponseStream?: boolean; + propertyKey: string | undefined; + type: "endpoint-field-v1" | "websocket-field-v1" | "webhook-field-v1"; }; export type MarkdownNode = { - level: number; - heading: string; - content: string; - children: MarkdownNode[]; + level: number; + heading: string; + content: string; + children: MarkdownNode[]; }; export type AlgoliaAdditionalProperties = - | { - type: "endpoint-field-v1"; - method: APIV1Read.HttpMethod; - endpointPath: APIV1Read.EndpointPathPart[]; - isResponseStream?: boolean; - } - | { - type: "websocket-field-v1"; - endpointPath: APIV1Read.EndpointPathPart[]; - } - | { - type: "webhook-field-v1"; - method: APIV1Read.HttpMethod; - endpointPath: APIV1Read.EndpointPathPart[]; - }; + | { + type: "endpoint-field-v1"; + method: APIV1Read.HttpMethod; + endpointPath: APIV1Read.EndpointPathPart[]; + isResponseStream?: boolean; + } + | { + type: "websocket-field-v1"; + endpointPath: APIV1Read.EndpointPathPart[]; + } + | { + type: "webhook-field-v1"; + method: APIV1Read.HttpMethod; + endpointPath: APIV1Read.EndpointPathPart[]; + }; diff --git a/servers/fdr/src/services/auth/AuthService.ts b/servers/fdr/src/services/auth/AuthService.ts index 16c6c01e01..f4583b4e6c 100644 --- a/servers/fdr/src/services/auth/AuthService.ts +++ b/servers/fdr/src/services/auth/AuthService.ts @@ -1,177 +1,211 @@ import { FernVenusApi, FernVenusApiClient } from "@fern-api/venus-api-sdk"; import winston from "winston"; import { FernRegistryError } from "../../api/generated"; -import { UnauthorizedError, UnavailableError, UserNotInOrgError } from "../../api/generated/api"; +import { + UnauthorizedError, + UnavailableError, + UserNotInOrgError, +} from "../../api/generated/api"; import type { FdrApplication, FdrConfig } from "../../app"; export type OrgIdsResponse = SuccessOrgIdsResponse | ErrorOrgIdsResponse; export interface SuccessOrgIdsResponse { - type: "success"; - orgIds: Set; + type: "success"; + orgIds: Set; } export interface ErrorOrgIdsResponse { - type: "error"; - err: FernRegistryError; + type: "error"; + err: FernRegistryError; } export interface AuthService { - checkUserBelongsToOrg({ authHeader, orgId }: { authHeader: string | undefined; orgId: string }): Promise; + checkUserBelongsToOrg({ + authHeader, + orgId, + }: { + authHeader: string | undefined; + orgId: string; + }): Promise; - getOrgIdsFromAuthHeader({ authHeader }: { authHeader: string | undefined }): Promise; - checkOrgHasSnippetsApiAccess({ - authHeader, - orgId, - failHard, - }: { - authHeader: string | undefined; - orgId: string; - failHard?: boolean; - }): Promise; - checkOrgHasSnippetTemplateAccess({ - authHeader, - orgId, - failHard, - }: { - authHeader: string | undefined; - orgId: string; - failHard?: boolean; - }): Promise; + getOrgIdsFromAuthHeader({ + authHeader, + }: { + authHeader: string | undefined; + }): Promise; + checkOrgHasSnippetsApiAccess({ + authHeader, + orgId, + failHard, + }: { + authHeader: string | undefined; + orgId: string; + failHard?: boolean; + }): Promise; + checkOrgHasSnippetTemplateAccess({ + authHeader, + orgId, + failHard, + }: { + authHeader: string | undefined; + orgId: string; + failHard?: boolean; + }): Promise; } export class AuthServiceImpl implements AuthService { - private logger: winston.Logger; + private logger: winston.Logger; - constructor(private readonly app: FdrApplication) { - this.logger = app.logger; - } + constructor(private readonly app: FdrApplication) { + this.logger = app.logger; + } - async getOrgIdsFromAuthHeader({ authHeader }: { authHeader: string | undefined }): Promise { - if (authHeader == null) { - return { - type: "error", - err: new UnauthorizedError("Authorization header was not specified"), - }; - } - const token = getTokenFromAuthHeader(authHeader); - const venus = getVenusClient({ - config: this.app.config, - token, - }); - const response = await venus.organization.getOrgIdsFromToken(); - if (!response.ok) { - this.logger.error("Failed to make request to venus", response.error); - return { - type: "error", - err: new UnavailableError("Failed to resolve organizations"), - }; - } - this.logger.error(`User belongs to organizations: ${response.body}`); - return { - type: "success", - orgIds: new Set(response.body), - }; + async getOrgIdsFromAuthHeader({ + authHeader, + }: { + authHeader: string | undefined; + }): Promise { + if (authHeader == null) { + return { + type: "error", + err: new UnauthorizedError("Authorization header was not specified"), + }; + } + const token = getTokenFromAuthHeader(authHeader); + const venus = getVenusClient({ + config: this.app.config, + token, + }); + const response = await venus.organization.getOrgIdsFromToken(); + if (!response.ok) { + this.logger.error("Failed to make request to venus", response.error); + return { + type: "error", + err: new UnavailableError("Failed to resolve organizations"), + }; } + this.logger.error(`User belongs to organizations: ${response.body}`); + return { + type: "success", + orgIds: new Set(response.body), + }; + } - // TODO: cache this so we don't make a round-trip to venus for every request - async checkUserBelongsToOrg({ - authHeader, - orgId, - }: { - authHeader: string | undefined; - orgId: string; - }): Promise { - if (authHeader == null) { - throw new UnauthorizedError("Authorization header was not specified"); - } - const token = getTokenFromAuthHeader(authHeader); - const venus = getVenusClient({ - config: this.app.config, - token, - }); - const response = await venus.organization.isMember(FernVenusApi.OrganizationId(orgId)); - if (!response.ok) { - this.logger.error("Failed to make request to venus", response.error); - throw new UnavailableError("Failed to resolve user's organizations"); - } - const belongsToOrg = response.body; - if (!belongsToOrg) { - throw new UserNotInOrgError("User does not belong to organization"); - } + // TODO: cache this so we don't make a round-trip to venus for every request + async checkUserBelongsToOrg({ + authHeader, + orgId, + }: { + authHeader: string | undefined; + orgId: string; + }): Promise { + if (authHeader == null) { + throw new UnauthorizedError("Authorization header was not specified"); } + const token = getTokenFromAuthHeader(authHeader); + const venus = getVenusClient({ + config: this.app.config, + token, + }); + const response = await venus.organization.isMember( + FernVenusApi.OrganizationId(orgId) + ); + if (!response.ok) { + this.logger.error("Failed to make request to venus", response.error); + throw new UnavailableError("Failed to resolve user's organizations"); + } + const belongsToOrg = response.body; + if (!belongsToOrg) { + throw new UserNotInOrgError("User does not belong to organization"); + } + } - async checkOrgHasSnippetsApiAccess({ - authHeader, - orgId, - failHard, - }: { - authHeader: string | undefined; - orgId: string; - failHard?: boolean; - }): Promise { - if (authHeader == null) { - throw new UnauthorizedError("Authorization header was not specified"); - } - await this.checkUserBelongsToOrg({ authHeader, orgId }); - const token = getTokenFromAuthHeader(authHeader); - const venus = getVenusClient({ - config: this.app.config, - token, - }); + async checkOrgHasSnippetsApiAccess({ + authHeader, + orgId, + failHard, + }: { + authHeader: string | undefined; + orgId: string; + failHard?: boolean; + }): Promise { + if (authHeader == null) { + throw new UnauthorizedError("Authorization header was not specified"); + } + await this.checkUserBelongsToOrg({ authHeader, orgId }); + const token = getTokenFromAuthHeader(authHeader); + const venus = getVenusClient({ + config: this.app.config, + token, + }); - const orgResponse = await venus.organization.get(FernVenusApi.OrganizationId(orgId)); - if (!orgResponse.ok) { - this.logger.error("Failed to make request to venus", orgResponse.error); - throw new UnavailableError("Failed to resolve user's organizations"); - } - const org = orgResponse.body; - if (failHard && !org.snippetsApiAccessEnabled) { - throw new UnauthorizedError("Organization does not have snippets API access"); - } - return org.snippetsApiAccessEnabled; + const orgResponse = await venus.organization.get( + FernVenusApi.OrganizationId(orgId) + ); + if (!orgResponse.ok) { + this.logger.error("Failed to make request to venus", orgResponse.error); + throw new UnavailableError("Failed to resolve user's organizations"); + } + const org = orgResponse.body; + if (failHard && !org.snippetsApiAccessEnabled) { + throw new UnauthorizedError( + "Organization does not have snippets API access" + ); } + return org.snippetsApiAccessEnabled; + } - async checkOrgHasSnippetTemplateAccess({ - authHeader, - orgId, - failHard, - }: { - authHeader: string | undefined; - orgId: string; - failHard?: boolean; - }): Promise { - if (authHeader == null) { - throw new UnauthorizedError("Authorization header was not specified"); - } - await this.checkUserBelongsToOrg({ authHeader, orgId }); - const token = getTokenFromAuthHeader(authHeader); - const venus = getVenusClient({ - config: this.app.config, - token, - }); + async checkOrgHasSnippetTemplateAccess({ + authHeader, + orgId, + failHard, + }: { + authHeader: string | undefined; + orgId: string; + failHard?: boolean; + }): Promise { + if (authHeader == null) { + throw new UnauthorizedError("Authorization header was not specified"); + } + await this.checkUserBelongsToOrg({ authHeader, orgId }); + const token = getTokenFromAuthHeader(authHeader); + const venus = getVenusClient({ + config: this.app.config, + token, + }); - const orgResponse = await venus.organization.get(FernVenusApi.OrganizationId(orgId)); - if (!orgResponse.ok) { - this.logger.error("Failed to make request to venus", orgResponse.error); - throw new UnavailableError("Failed to resolve user's organizations"); - } - const org = orgResponse.body; - if (failHard && !org.snippetTemplatesAccessEnabled) { - throw new UnauthorizedError("Organization does not have snippets API access"); - } - return org.snippetTemplatesAccessEnabled; + const orgResponse = await venus.organization.get( + FernVenusApi.OrganizationId(orgId) + ); + if (!orgResponse.ok) { + this.logger.error("Failed to make request to venus", orgResponse.error); + throw new UnavailableError("Failed to resolve user's organizations"); + } + const org = orgResponse.body; + if (failHard && !org.snippetTemplatesAccessEnabled) { + throw new UnauthorizedError( + "Organization does not have snippets API access" + ); } + return org.snippetTemplatesAccessEnabled; + } } -function getVenusClient({ config, token }: { config: FdrConfig; token?: string }): FernVenusApiClient { - return new FernVenusApiClient({ - environment: config.venusUrl, - token, - }); +function getVenusClient({ + config, + token, +}: { + config: FdrConfig; + token?: string; +}): FernVenusApiClient { + return new FernVenusApiClient({ + environment: config.venusUrl, + token, + }); } const BEARER_REGEX = /^bearer\s+/i; export function getTokenFromAuthHeader(authHeader: string) { - return authHeader.replace(BEARER_REGEX, ""); + return authHeader.replace(BEARER_REGEX, ""); } diff --git a/servers/fdr/src/services/db/DatabaseService.ts b/servers/fdr/src/services/db/DatabaseService.ts index 63987cdfd3..e12dace847 100644 --- a/servers/fdr/src/services/db/DatabaseService.ts +++ b/servers/fdr/src/services/db/DatabaseService.ts @@ -2,35 +2,37 @@ import { APIV1Db } from "@fern-api/fdr-sdk"; import { PrismaClient } from "@prisma/client"; export interface DatabaseService { - readonly prisma: PrismaClient; + readonly prisma: PrismaClient; - getApiDefinition(id: string): Promise; + getApiDefinition(id: string): Promise; - markIndexForDeletion(indexId: string): Promise; + markIndexForDeletion(indexId: string): Promise; } export class DatabaseServiceImpl implements DatabaseService { - public constructor(public readonly prisma: PrismaClient) {} + public constructor(public readonly prisma: PrismaClient) {} - public async getApiDefinition(id: string) { - const record = await this.prisma.apiDefinitionsV2.findFirst({ - where: { - apiDefinitionId: id, - }, - }); - if (!record) { - return null; - } - try { - return JSON.parse(record.definition.toString()) as APIV1Db.DbApiDefinition; - } catch { - return null; - } + public async getApiDefinition(id: string) { + const record = await this.prisma.apiDefinitionsV2.findFirst({ + where: { + apiDefinitionId: id, + }, + }); + if (!record) { + return null; } - - public async markIndexForDeletion(indexId: string) { - await this.prisma.overwrittenAlgoliaIndex.create({ - data: { indexId }, - }); + try { + return JSON.parse( + record.definition.toString() + ) as APIV1Db.DbApiDefinition; + } catch { + return null; } + } + + public async markIndexForDeletion(indexId: string) { + await this.prisma.overwrittenAlgoliaIndex.create({ + data: { indexId }, + }); + } } diff --git a/servers/fdr/src/services/docs-cache/DocsDefinitionCache.ts b/servers/fdr/src/services/docs-cache/DocsDefinitionCache.ts index 82eef5401d..ba09bf5422 100644 --- a/servers/fdr/src/services/docs-cache/DocsDefinitionCache.ts +++ b/servers/fdr/src/services/docs-cache/DocsDefinitionCache.ts @@ -2,7 +2,10 @@ import { DocsV1Db, DocsV1Read, DocsV2Read } from "@fern-api/fdr-sdk"; import { AuthType } from "@prisma/client"; import { DomainNotRegisteredError } from "../../api/generated/api/resources/docs/resources/v2/resources/read"; import { FdrApplication } from "../../app"; -import { getDocsDefinition, getDocsForDomain } from "../../controllers/docs/v1/getDocsReadService"; +import { + getDocsDefinition, + getDocsForDomain, +} from "../../controllers/docs/v1/getDocsReadService"; import { DocsRegistrationInfo } from "../../controllers/docs/v2/getDocsWriteV2Service"; import { FdrDao } from "../../db"; import type { IndexSegment } from "../algolia"; @@ -14,33 +17,35 @@ import { FernRegistry } from "../../api/generated"; const DOCS_DOMAIN_REGX = /^([^.\s]+)/; export interface DocsDefinitionCache { - getDocsForUrl(request: { url: URL }): Promise; + getDocsForUrl(request: { + url: URL; + }): Promise; - storeDocsForUrl({ - docsRegistrationInfo, - dbDocsDefinition, - indexSegments, - }: { - docsRegistrationInfo: DocsRegistrationInfo; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise; + storeDocsForUrl({ + docsRegistrationInfo, + dbDocsDefinition, + indexSegments, + }: { + docsRegistrationInfo: DocsRegistrationInfo; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise; - replaceDocsForInstanceId({ - instanceId, - dbDocsDefinition, - indexSegments, - }: { - instanceId: string; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise; + replaceDocsForInstanceId({ + instanceId, + dbDocsDefinition, + indexSegments, + }: { + instanceId: string; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise; - initialize(): Promise; + initialize(): Promise; - isInitialized(): boolean; + isInitialized(): boolean; - invalidateCache(url: URL): Promise; + invalidateCache(url: URL): Promise; } /** @@ -54,230 +59,260 @@ const SEMANTIC_VERSION = "v3"; * In other words, only add optional properties. */ export interface CachedDocsResponse { - /** Adding a version to the cached response to allow for breaks in the future. */ - version: typeof SEMANTIC_VERSION; - updatedTime: Date; - response: DocsV2Read.LoadDocsForUrlResponse; - dbFiles: Record; - isPrivate: boolean; - usesPublicS3?: boolean; + /** Adding a version to the cached response to allow for breaks in the future. */ + version: typeof SEMANTIC_VERSION; + updatedTime: Date; + response: DocsV2Read.LoadDocsForUrlResponse; + dbFiles: Record; + isPrivate: boolean; + usesPublicS3?: boolean; } export class DocsDefinitionCacheImpl implements DocsDefinitionCache { - private localDocsCache: LocalDocsDefinitionStore; - private redisDocsCache: RedisDocsDefinitionStore | undefined; - private DOCS_WRITE_MONITOR: Record = {}; - private initialized: boolean = false; + private localDocsCache: LocalDocsDefinitionStore; + private redisDocsCache: RedisDocsDefinitionStore | undefined; + private DOCS_WRITE_MONITOR: Record = {}; + private initialized: boolean = false; - constructor( - private readonly app: FdrApplication, - private readonly dao: FdrDao, - localDocsCache: LocalDocsDefinitionStore, - redisDocsCache: RedisDocsDefinitionStore | undefined, - ) { - this.localDocsCache = localDocsCache; - this.redisDocsCache = redisDocsCache; - } + constructor( + private readonly app: FdrApplication, + private readonly dao: FdrDao, + localDocsCache: LocalDocsDefinitionStore, + redisDocsCache: RedisDocsDefinitionStore | undefined + ) { + this.localDocsCache = localDocsCache; + this.redisDocsCache = redisDocsCache; + } - public isInitialized(): boolean { - return this.initialized; - } + public isInitialized(): boolean { + return this.initialized; + } - public async initialize(): Promise { - if (this.redisDocsCache) { - await this.redisDocsCache.initializeCache(); - } - this.initialized = true; + public async initialize(): Promise { + if (this.redisDocsCache) { + await this.redisDocsCache.initializeCache(); } + this.initialized = true; + } - // allows us to block reads from writing to the cache while we are updating it - private getDocsWriteMonitor(hostname: string): Semaphore { - let monitor = this.DOCS_WRITE_MONITOR[hostname]; - if (monitor == null) { - monitor = new Semaphore(1); - this.DOCS_WRITE_MONITOR[hostname] = monitor; - } - return monitor; + // allows us to block reads from writing to the cache while we are updating it + private getDocsWriteMonitor(hostname: string): Semaphore { + let monitor = this.DOCS_WRITE_MONITOR[hostname]; + if (monitor == null) { + monitor = new Semaphore(1); + this.DOCS_WRITE_MONITOR[hostname] = monitor; } + return monitor; + } - public async invalidateCache(url: URL): Promise { - if (this.redisDocsCache) { - await this.redisDocsCache.delete({ url }); - } - this.localDocsCache.delete({ url }); + public async invalidateCache(url: URL): Promise { + if (this.redisDocsCache) { + await this.redisDocsCache.delete({ url }); } + this.localDocsCache.delete({ url }); + } - public async getDocsForUrl({ url }: { url: URL }): Promise { - const cachedResponse = await this.getDocsForUrlFromCache({ url }); - if (cachedResponse != null) { - this.app.logger.info(`Cache HIT for ${url}`); - const filesV2: Record = Object.fromEntries( - await Promise.all( - Object.entries(cachedResponse.dbFiles).map(async ([fileId, dbFileInfo]) => { - const presignedUrl = await this.app.services.s3.getPresignedDocsAssetsDownloadUrl({ - key: dbFileInfo.s3Key, - isPrivate: cachedResponse.usesPublicS3 === true ? false : true, - }); - - switch (dbFileInfo.type) { - case "image": { - const { s3Key, ...image } = dbFileInfo; - return [fileId, { ...image, url: presignedUrl }]; - } - default: - return [fileId, { type: "url", url: presignedUrl }]; - } - }), - ), - ); + public async getDocsForUrl({ + url, + }: { + url: URL; + }): Promise { + const cachedResponse = await this.getDocsForUrlFromCache({ url }); + if (cachedResponse != null) { + this.app.logger.info(`Cache HIT for ${url}`); + const filesV2: Record = Object.fromEntries( + await Promise.all( + Object.entries(cachedResponse.dbFiles).map( + async ([fileId, dbFileInfo]) => { + const presignedUrl = + await this.app.services.s3.getPresignedDocsAssetsDownloadUrl({ + key: dbFileInfo.s3Key, + isPrivate: + cachedResponse.usesPublicS3 === true ? false : true, + }); - // we always pull updated s3 URLs - return { - ...cachedResponse.response, - definition: { - ...cachedResponse.response.definition, - filesV2, - }, - }; - } + switch (dbFileInfo.type) { + case "image": { + const { s3Key, ...image } = dbFileInfo; + return [fileId, { ...image, url: presignedUrl }]; + } + default: + return [fileId, { type: "url", url: presignedUrl }]; + } + } + ) + ) + ); - this.app.logger.info(`Cache MISS for ${url}`); - const dbResponse = await this.getDocsForUrlFromDatabase({ url }); + // we always pull updated s3 URLs + return { + ...cachedResponse.response, + definition: { + ...cachedResponse.response.definition, + filesV2, + }, + }; + } - // we don't want to cache from READ if we are currently updating the cache via WRITE - if (!this.getDocsWriteMonitor(url.hostname).isLocked()) { - await this.cacheResponse({ url, value: dbResponse }); - } + this.app.logger.info(`Cache MISS for ${url}`); + const dbResponse = await this.getDocsForUrlFromDatabase({ url }); - return dbResponse.response; + // we don't want to cache from READ if we are currently updating the cache via WRITE + if (!this.getDocsWriteMonitor(url.hostname).isLocked()) { + await this.cacheResponse({ url, value: dbResponse }); } - public async storeDocsForUrl({ - docsRegistrationInfo, - dbDocsDefinition, - indexSegments, - }: { - docsRegistrationInfo: DocsRegistrationInfo; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise { - const resp = await this.dao.docsV2().storeDocsDefinition({ - docsRegistrationInfo, - dbDocsDefinition, - indexSegments, - }); + return dbResponse.response; + } - // cache fern URL + custom URLs - await Promise.all( - resp.domains.map(async (docsUrl) => { - // the write monitor is used to block reads from writing to the cache while we are updating it - // it also prevents two cache-write operations to the same hostname from happening at the same time - return await this.getDocsWriteMonitor(docsUrl.hostname).use(async () => { - const url = docsUrl.toURL(); - const dbResponse = await this.getDocsForUrlFromDatabase({ url }); - await this.cacheResponse({ url, value: dbResponse }); - }); - }), + public async storeDocsForUrl({ + docsRegistrationInfo, + dbDocsDefinition, + indexSegments, + }: { + docsRegistrationInfo: DocsRegistrationInfo; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise { + const resp = await this.dao.docsV2().storeDocsDefinition({ + docsRegistrationInfo, + dbDocsDefinition, + indexSegments, + }); + + // cache fern URL + custom URLs + await Promise.all( + resp.domains.map(async (docsUrl) => { + // the write monitor is used to block reads from writing to the cache while we are updating it + // it also prevents two cache-write operations to the same hostname from happening at the same time + return await this.getDocsWriteMonitor(docsUrl.hostname).use( + async () => { + const url = docsUrl.toURL(); + const dbResponse = await this.getDocsForUrlFromDatabase({ url }); + await this.cacheResponse({ url, value: dbResponse }); + } ); - } + }) + ); + } - public async replaceDocsForInstanceId({ - instanceId, - dbDocsDefinition, - indexSegments, - }: { - instanceId: string; - dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; - indexSegments: IndexSegment[]; - }): Promise { - const resp = await this.dao.docsV2().replaceDocsDefinition({ - instanceId, - dbDocsDefinition, - indexSegments, - }); + public async replaceDocsForInstanceId({ + instanceId, + dbDocsDefinition, + indexSegments, + }: { + instanceId: string; + dbDocsDefinition: DocsV1Db.DocsDefinitionDb.V3; + indexSegments: IndexSegment[]; + }): Promise { + const resp = await this.dao.docsV2().replaceDocsDefinition({ + instanceId, + dbDocsDefinition, + indexSegments, + }); - // cache fern URL + custom URLs - await Promise.all( - resp.domains.map(async (docsUrl) => { - // the write monitor is used to block reads from writing to the cache while we are updating it - // it also prevents two cache-write operations to the same hostname from happening at the same time - return await this.getDocsWriteMonitor(docsUrl.hostname).use(async () => { - const url = docsUrl.toURL(); - const dbResponse = await this.getDocsForUrlFromDatabase({ url }); - await this.cacheResponse({ url, value: dbResponse }); - }); - }), + // cache fern URL + custom URLs + await Promise.all( + resp.domains.map(async (docsUrl) => { + // the write monitor is used to block reads from writing to the cache while we are updating it + // it also prevents two cache-write operations to the same hostname from happening at the same time + return await this.getDocsWriteMonitor(docsUrl.hostname).use( + async () => { + const url = docsUrl.toURL(); + const dbResponse = await this.getDocsForUrlFromDatabase({ url }); + await this.cacheResponse({ url, value: dbResponse }); + } ); - } + }) + ); + } - private async cacheResponse({ url, value }: { url: URL; value: CachedDocsResponse }): Promise { - if (this.redisDocsCache) { - await this.redisDocsCache.set({ url, value }); - } - this.localDocsCache.set({ url, value }); + private async cacheResponse({ + url, + value, + }: { + url: URL; + value: CachedDocsResponse; + }): Promise { + if (this.redisDocsCache) { + await this.redisDocsCache.set({ url, value }); } + this.localDocsCache.set({ url, value }); + } - private async getDocsForUrlFromCache({ url }: { url: URL }): Promise { - let record: CachedDocsResponse | null = null; - if (this.redisDocsCache) { - record = await this.redisDocsCache.get({ url }); - } else { - record = this.localDocsCache.get({ url }) ?? null; - } - if (record != null && record.version !== SEMANTIC_VERSION) { - return null; - } - return record; + private async getDocsForUrlFromCache({ + url, + }: { + url: URL; + }): Promise { + let record: CachedDocsResponse | null = null; + if (this.redisDocsCache) { + record = await this.redisDocsCache.get({ url }); + } else { + record = this.localDocsCache.get({ url }) ?? null; } + if (record != null && record.version !== SEMANTIC_VERSION) { + return null; + } + return record; + } - private async getDocsForUrlFromDatabase({ url }: { url: URL }): Promise { - const dbDocs = await this.dao.docsV2().loadDocsForURL(url); - if (dbDocs != null) { - const definition = await getDocsDefinition({ - app: this.app, - docsDbDefinition: dbDocs.docsDefinition, - docsV2: dbDocs, - }); - return { - version: "v3", - updatedTime: dbDocs.updatedTime, - dbFiles: dbDocs.docsDefinition.files, - response: { - orgId: dbDocs.orgId, - baseUrl: { - domain: dbDocs.domain, - basePath: dbDocs.path.trim() === "" ? undefined : dbDocs.path.trim(), - }, - definition, - lightModeEnabled: definition.config.colorsV3?.type !== "dark", - }, - isPrivate: dbDocs.authType !== AuthType.PUBLIC, - usesPublicS3: dbDocs.hasPublicS3Assets, - }; - } else { - // TODO(dsinghvi): Stop serving the v1 APIs - // delegate to V1 - const v1Domain = url.hostname.match(DOCS_DOMAIN_REGX)?.[1]; - if (v1Domain == null) { - throw new DomainNotRegisteredError(); - } - const v1Docs = await getDocsForDomain({ app: this.app, domain: v1Domain }); - return { - version: "v3", - updatedTime: new Date(), - dbFiles: v1Docs.dbFiles ?? {}, - response: { - orgId: FernRegistry.OrgId("dummy"), // TODO(dsinghvi): Stop serving the v1 APIs - baseUrl: { - domain: url.hostname, - basePath: undefined, - }, - definition: v1Docs.response, - lightModeEnabled: v1Docs.response.config.colorsV3?.type !== "dark", - }, - isPrivate: false, - usesPublicS3: false, - }; - } + private async getDocsForUrlFromDatabase({ + url, + }: { + url: URL; + }): Promise { + const dbDocs = await this.dao.docsV2().loadDocsForURL(url); + if (dbDocs != null) { + const definition = await getDocsDefinition({ + app: this.app, + docsDbDefinition: dbDocs.docsDefinition, + docsV2: dbDocs, + }); + return { + version: "v3", + updatedTime: dbDocs.updatedTime, + dbFiles: dbDocs.docsDefinition.files, + response: { + orgId: dbDocs.orgId, + baseUrl: { + domain: dbDocs.domain, + basePath: + dbDocs.path.trim() === "" ? undefined : dbDocs.path.trim(), + }, + definition, + lightModeEnabled: definition.config.colorsV3?.type !== "dark", + }, + isPrivate: dbDocs.authType !== AuthType.PUBLIC, + usesPublicS3: dbDocs.hasPublicS3Assets, + }; + } else { + // TODO(dsinghvi): Stop serving the v1 APIs + // delegate to V1 + const v1Domain = url.hostname.match(DOCS_DOMAIN_REGX)?.[1]; + if (v1Domain == null) { + throw new DomainNotRegisteredError(); + } + const v1Docs = await getDocsForDomain({ + app: this.app, + domain: v1Domain, + }); + return { + version: "v3", + updatedTime: new Date(), + dbFiles: v1Docs.dbFiles ?? {}, + response: { + orgId: FernRegistry.OrgId("dummy"), // TODO(dsinghvi): Stop serving the v1 APIs + baseUrl: { + domain: url.hostname, + basePath: undefined, + }, + definition: v1Docs.response, + lightModeEnabled: v1Docs.response.config.colorsV3?.type !== "dark", + }, + isPrivate: false, + usesPublicS3: false, + }; } + } } diff --git a/servers/fdr/src/services/docs-cache/LocalDocsDefinitionStore.ts b/servers/fdr/src/services/docs-cache/LocalDocsDefinitionStore.ts index 1092d0200f..f3ad8967c2 100644 --- a/servers/fdr/src/services/docs-cache/LocalDocsDefinitionStore.ts +++ b/servers/fdr/src/services/docs-cache/LocalDocsDefinitionStore.ts @@ -1,22 +1,22 @@ import { CachedDocsResponse } from "./DocsDefinitionCache"; export default class LocalDocsDefinitionStore { - private localCache: Record; + private localCache: Record; - public constructor() { - this.localCache = {}; - } + public constructor() { + this.localCache = {}; + } - get({ url }: { url: URL }): CachedDocsResponse | undefined { - return this.localCache[url.hostname]; - } + get({ url }: { url: URL }): CachedDocsResponse | undefined { + return this.localCache[url.hostname]; + } - set({ url, value }: { url: URL; value: CachedDocsResponse }): void { - this.localCache[(url.hostname, JSON.stringify(value))]; - } + set({ url, value }: { url: URL; value: CachedDocsResponse }): void { + this.localCache[(url.hostname, JSON.stringify(value))]; + } - delete({ url }: { url: URL }): void { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.localCache[url.hostname]; - } + delete({ url }: { url: URL }): void { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.localCache[url.hostname]; + } } diff --git a/servers/fdr/src/services/docs-cache/RedisDocsDefinitionStore.ts b/servers/fdr/src/services/docs-cache/RedisDocsDefinitionStore.ts index f6d48c7c5d..39cf1304b9 100644 --- a/servers/fdr/src/services/docs-cache/RedisDocsDefinitionStore.ts +++ b/servers/fdr/src/services/docs-cache/RedisDocsDefinitionStore.ts @@ -3,45 +3,61 @@ import { LOGGER } from "../../app/FdrApplication"; import { CachedDocsResponse } from "./DocsDefinitionCache"; export declare namespace RedisDocsDefinitionStore { - interface Args { - clusterModeEnabled: boolean; - cacheEndpointUrl: string; - } + interface Args { + clusterModeEnabled: boolean; + cacheEndpointUrl: string; + } } export default class RedisDocsDefinitionStore { - private client: Cluster | Redis; + private client: Cluster | Redis; - public constructor({ cacheEndpointUrl, clusterModeEnabled }: RedisDocsDefinitionStore.Args) { - this.client = clusterModeEnabled - ? new Redis.Cluster([cacheEndpointUrl], { - lazyConnect: true, - enableOfflineQueue: false, - }) - : new Redis(cacheEndpointUrl, { - lazyConnect: true, - enableOfflineQueue: false, - }); - this.client.on("error", (error) => LOGGER.error("Encountered error from redis", error)); - } + public constructor({ + cacheEndpointUrl, + clusterModeEnabled, + }: RedisDocsDefinitionStore.Args) { + this.client = clusterModeEnabled + ? new Redis.Cluster([cacheEndpointUrl], { + lazyConnect: true, + enableOfflineQueue: false, + }) + : new Redis(cacheEndpointUrl, { + lazyConnect: true, + enableOfflineQueue: false, + }); + this.client.on("error", (error) => + LOGGER.error("Encountered error from redis", error) + ); + } - public async initializeCache(): Promise { - await this.client.connect(); - } + public async initializeCache(): Promise { + await this.client.connect(); + } - public async get({ url }: { url: URL }): Promise { - const result = await this.client.get(url.hostname); - if (result) { - return JSON.parse(result); - } - return null; + public async get({ url }: { url: URL }): Promise { + const result = await this.client.get(url.hostname); + if (result) { + return JSON.parse(result); } + return null; + } - public async set({ url, value }: { url: URL; value: CachedDocsResponse }): Promise { - await this.client.set(url.hostname, JSON.stringify(value), "EX", 60 * 60 * 24 * 7); - } + public async set({ + url, + value, + }: { + url: URL; + value: CachedDocsResponse; + }): Promise { + await this.client.set( + url.hostname, + JSON.stringify(value), + "EX", + 60 * 60 * 24 * 7 + ); + } - public async delete({ url }: { url: URL }): Promise { - await this.client.del(url.hostname); - } + public async delete({ url }: { url: URL }): Promise { + await this.client.del(url.hostname); + } } diff --git a/servers/fdr/src/services/revalidator/RevalidatorService.ts b/servers/fdr/src/services/revalidator/RevalidatorService.ts index d853b85c57..d03781c839 100644 --- a/servers/fdr/src/services/revalidator/RevalidatorService.ts +++ b/servers/fdr/src/services/revalidator/RevalidatorService.ts @@ -4,97 +4,100 @@ import { FdrApplication } from "../../app"; import { ParsedBaseUrl } from "../../util/ParsedBaseUrl"; export type RevalidatedPathsResponse = { - successful: FernDocs.SuccessfulRevalidation[]; - failed: FernDocs.FailedRevalidation[]; - revalidationFailed: boolean; + successful: FernDocs.SuccessfulRevalidation[]; + failed: FernDocs.FailedRevalidation[]; + revalidationFailed: boolean; }; export interface RevalidatorService { - revalidate(params: { baseUrl: ParsedBaseUrl; app: FdrApplication }): Promise; + revalidate(params: { + baseUrl: ParsedBaseUrl; + app: FdrApplication; + }): Promise; } export class RevalidatorServiceImpl implements RevalidatorService { - // private readonly semaphore = new Semaphore(50); + // private readonly semaphore = new Semaphore(50); - /** - * NOTE on basepath revalidation: - * - * When the baseUrl.path is not null, the custom domain is re-written. Thus, - * /api/revalidate-all does not exist on the root, but `/base/path/api/revalidate-all` does (rewritten via frontend middleware). - * - * Behind the scenes, the revalidation request is sent to the original domain, i.e. org.docs.buildwithfern.com. - * - * Example prefetch request: - * https://custom-domain.com/path/_next/data/.../static/custom-domain.com/path.json is rewritten to: - * https://org.docs.buildwithfern.com/path/_next/data/.../static/custom-domain.com/path.json - * - * So `/static/custom-domain.com/path` is the path we need to revalidate on org.docs.buildwithfern.com - */ + /** + * NOTE on basepath revalidation: + * + * When the baseUrl.path is not null, the custom domain is re-written. Thus, + * /api/revalidate-all does not exist on the root, but `/base/path/api/revalidate-all` does (rewritten via frontend middleware). + * + * Behind the scenes, the revalidation request is sent to the original domain, i.e. org.docs.buildwithfern.com. + * + * Example prefetch request: + * https://custom-domain.com/path/_next/data/.../static/custom-domain.com/path.json is rewritten to: + * https://org.docs.buildwithfern.com/path/_next/data/.../static/custom-domain.com/path.json + * + * So `/static/custom-domain.com/path` is the path we need to revalidate on org.docs.buildwithfern.com + */ - public async revalidate({ - baseUrl, - app, - }: { - baseUrl: ParsedBaseUrl; - app?: FdrApplication; - }): Promise { - // let revalidationFailed = false; - try { - const client = new FernRevalidationClient({ - environment: baseUrl.toURL().toString(), - }); - app?.logger.log("Revalidating paths at", baseUrl.toURL().toString()); - await client.revalidateAllV3({ - host: baseUrl.hostname, - basePath: baseUrl.path != null ? baseUrl.path : "", - xFernHost: baseUrl.hostname, - }); - return { - successful: [], - failed: [], - revalidationFailed: false, - }; - } catch (e) { - app?.logger.error("Failed to revalidate paths", e); - // revalidationFailed = true; - console.log(e); - return { - successful: [], - failed: [], - revalidationFailed: true, - }; - } + public async revalidate({ + baseUrl, + app, + }: { + baseUrl: ParsedBaseUrl; + app?: FdrApplication; + }): Promise { + // let revalidationFailed = false; + try { + const client = new FernRevalidationClient({ + environment: baseUrl.toURL().toString(), + }); + app?.logger.log("Revalidating paths at", baseUrl.toURL().toString()); + await client.revalidateAllV3({ + host: baseUrl.hostname, + basePath: baseUrl.path != null ? baseUrl.path : "", + xFernHost: baseUrl.hostname, + }); + return { + successful: [], + failed: [], + revalidationFailed: false, + }; + } catch (e) { + app?.logger.error("Failed to revalidate paths", e); + // revalidationFailed = true; + console.log(e); + return { + successful: [], + failed: [], + revalidationFailed: true, + }; + } - // let revalidationFailed = false; - // try { - // const client = new FernDocsClient({ - // environment: baseUrl.toURL().toString(), - // }); - // app?.logger.log("Revalidating paths at", baseUrl.toURL().toString()); - // const page = await client.revalidation.revalidateAllV4({ limit: 100 }); + // let revalidationFailed = false; + // try { + // const client = new FernDocsClient({ + // environment: baseUrl.toURL().toString(), + // }); + // app?.logger.log("Revalidating paths at", baseUrl.toURL().toString()); + // const page = await client.revalidation.revalidateAllV4({ limit: 100 }); - // const successful: FernDocs.SuccessfulRevalidation[] = []; - // const failed: FernDocs.FailedRevalidation[] = []; + // const successful: FernDocs.SuccessfulRevalidation[] = []; + // const failed: FernDocs.FailedRevalidation[] = []; - // for await (const result of page) { - // if (!result.success) { - // failed.push(result); - // app?.logger.error(`Revalidation failed for ${result.url}`, result.error); - // } else { - // successful.push(result); - // } - // } + // for await (const result of page) { + // if (!result.success) { + // failed.push(result); + // app?.logger.error(`Revalidation failed for ${result.url}`, result.error); + // } else { + // successful.push(result); + // } + // } - // return { - // failed, - // successful, - // revalidationFailed: false, - // }; - // } catch (e) { - // app?.logger.error("Failed to revalidate paths", e); - // revalidationFailed = true; - // console.log(e); - // return { failed: [], successful: [], revalidationFailed: true }; - // } - } + // return { + // failed, + // successful, + // revalidationFailed: false, + // }; + // } catch (e) { + // app?.logger.error("Failed to revalidate paths", e); + // revalidationFailed = true; + // console.log(e); + // return { failed: [], successful: [], revalidationFailed: true }; + // } + } } diff --git a/servers/fdr/src/services/revalidator/Semaphore.ts b/servers/fdr/src/services/revalidator/Semaphore.ts index 026f93b5d5..56a3f49ea7 100644 --- a/servers/fdr/src/services/revalidator/Semaphore.ts +++ b/servers/fdr/src/services/revalidator/Semaphore.ts @@ -1,37 +1,37 @@ export class Semaphore { - public count: number; - public readonly waiting: (() => void)[] = []; + public count: number; + public readonly waiting: (() => void)[] = []; - constructor(count: number) { - this.count = count; - } + constructor(count: number) { + this.count = count; + } - async acquire(): Promise { - if (this.count > 0) { - this.count--; - } else { - await new Promise((resolve) => this.waiting.push(resolve)); - } + async acquire(): Promise { + if (this.count > 0) { + this.count--; + } else { + await new Promise((resolve) => this.waiting.push(resolve)); } + } - release(): void { - this.count++; - const next = this.waiting.shift(); - if (next) { - next(); - } + release(): void { + this.count++; + const next = this.waiting.shift(); + if (next) { + next(); } + } - async use(fn: () => Promise): Promise { - await this.acquire(); - try { - return await fn(); - } finally { - this.release(); - } + async use(fn: () => Promise): Promise { + await this.acquire(); + try { + return await fn(); + } finally { + this.release(); } + } - isLocked(): boolean { - return this.count <= 0; - } + isLocked(): boolean { + return this.count <= 0; + } } diff --git a/servers/fdr/src/services/s3/S3Service.ts b/servers/fdr/src/services/s3/S3Service.ts index f254c61f15..43562775b2 100644 --- a/servers/fdr/src/services/s3/S3Service.ts +++ b/servers/fdr/src/services/s3/S3Service.ts @@ -1,6 +1,16 @@ -import { GetObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client } from "@aws-sdk/client-s3"; +import { + GetObjectCommand, + PutObjectCommand, + PutObjectCommandInput, + S3Client, +} from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { APIV1Write, DocsV1Write, DocsV2Write, FdrAPI } from "@fern-api/fdr-sdk"; +import { + APIV1Write, + DocsV1Write, + DocsV2Write, + FdrAPI, +} from "@fern-api/fdr-sdk"; import { v4 as uuidv4 } from "uuid"; import { Cache } from "../../Cache"; import { FernRegistry } from "../../api/generated"; @@ -9,274 +19,316 @@ import type { FdrConfig } from "../../app"; const ONE_WEEK_IN_SECONDS = 604800; export interface S3DocsFileInfo { - presignedUrl: DocsV1Write.FileS3UploadUrl; - key: string; - imageMetadata: - | { - width: number; - height: number; - blurDataUrl: string | undefined; - alt: string | undefined; - } - | undefined; + presignedUrl: DocsV1Write.FileS3UploadUrl; + key: string; + imageMetadata: + | { + width: number; + height: number; + blurDataUrl: string | undefined; + alt: string | undefined; + } + | undefined; } export interface S3ApiDefinitionSourceFileInfo { - presignedUrl: string; - key: string; + presignedUrl: string; + key: string; } export interface S3Service { - getPresignedDocsAssetsUploadUrls({ - domain, - filepaths, - images, - isPrivate, - }: { - domain: string; - filepaths: DocsV1Write.FilePath[]; - images: DocsV2Write.ImageFilePath[]; - isPrivate: boolean; - }): Promise>; + getPresignedDocsAssetsUploadUrls({ + domain, + filepaths, + images, + isPrivate, + }: { + domain: string; + filepaths: DocsV1Write.FilePath[]; + images: DocsV2Write.ImageFilePath[]; + isPrivate: boolean; + }): Promise>; - getPresignedDocsAssetsDownloadUrl({ key, isPrivate }: { key: string; isPrivate: boolean }): Promise; + getPresignedDocsAssetsDownloadUrl({ + key, + isPrivate, + }: { + key: string; + isPrivate: boolean; + }): Promise; - getPresignedApiDefinitionSourceUploadUrls({ - orgId, - apiId, - sources, - }: { - orgId: FernRegistry.OrgId; - apiId: FernRegistry.ApiId; - sources: Record; - }): Promise>; + getPresignedApiDefinitionSourceUploadUrls({ + orgId, + apiId, + sources, + }: { + orgId: FernRegistry.OrgId; + apiId: FernRegistry.ApiId; + sources: Record; + }): Promise>; - getPresignedApiDefinitionSourceDownloadUrl({ key }: { key: string }): Promise; + getPresignedApiDefinitionSourceDownloadUrl({ + key, + }: { + key: string; + }): Promise; } export class S3ServiceImpl implements S3Service { - private publicDocsCDNUrl: string; - private publicDocsS3: S3Client; - private privateDocsS3: S3Client; - private privateApiDefinitionSourceS3: S3Client; - private presignedDownloadUrlCache = new Cache(10_000, ONE_WEEK_IN_SECONDS); - - constructor(private readonly config: FdrConfig) { - this.publicDocsCDNUrl = config.cdnPublicDocsUrl; - this.publicDocsS3 = new S3Client({ - ...(config.publicDocsS3.urlOverride != null ? { endpoint: config.publicDocsS3.urlOverride } : {}), - region: config.publicDocsS3.bucketRegion, - credentials: { - accessKeyId: config.awsAccessKey, - secretAccessKey: config.awsSecretKey, - }, - }); - this.privateDocsS3 = new S3Client({ - ...(config.privateDocsS3.urlOverride != null ? { endpoint: config.privateDocsS3.urlOverride } : {}), - region: config.privateDocsS3.bucketRegion, - credentials: { - accessKeyId: config.awsAccessKey, - secretAccessKey: config.awsSecretKey, - }, - }); - this.privateApiDefinitionSourceS3 = new S3Client({ - ...(config.privateApiDefinitionSourceS3.urlOverride != null - ? { endpoint: config.privateApiDefinitionSourceS3.urlOverride } - : {}), - region: config.privateApiDefinitionSourceS3.bucketRegion, - credentials: { - accessKeyId: config.awsAccessKey, - secretAccessKey: config.awsSecretKey, - }, - }); - } + private publicDocsCDNUrl: string; + private publicDocsS3: S3Client; + private privateDocsS3: S3Client; + private privateApiDefinitionSourceS3: S3Client; + private presignedDownloadUrlCache = new Cache( + 10_000, + ONE_WEEK_IN_SECONDS + ); - async getPresignedDocsAssetsDownloadUrl({ - key, - isPrivate, - }: { - key: string; - isPrivate: boolean; - }): Promise { - if (isPrivate) { - // presigned url for private - const cachedUrl = this.presignedDownloadUrlCache.get(key); - if (cachedUrl != null && typeof cachedUrl === "string") { - return FdrAPI.Url(cachedUrl); - } - const command = new GetObjectCommand({ - Bucket: this.config.privateDocsS3.bucketName, - Key: key, - }); - const signedUrl = await getSignedUrl(this.privateDocsS3, command, { expiresIn: 604800 }); - this.presignedDownloadUrlCache.set(key, signedUrl); - return FdrAPI.Url(signedUrl); - } + constructor(private readonly config: FdrConfig) { + this.publicDocsCDNUrl = config.cdnPublicDocsUrl; + this.publicDocsS3 = new S3Client({ + ...(config.publicDocsS3.urlOverride != null + ? { endpoint: config.publicDocsS3.urlOverride } + : {}), + region: config.publicDocsS3.bucketRegion, + credentials: { + accessKeyId: config.awsAccessKey, + secretAccessKey: config.awsSecretKey, + }, + }); + this.privateDocsS3 = new S3Client({ + ...(config.privateDocsS3.urlOverride != null + ? { endpoint: config.privateDocsS3.urlOverride } + : {}), + region: config.privateDocsS3.bucketRegion, + credentials: { + accessKeyId: config.awsAccessKey, + secretAccessKey: config.awsSecretKey, + }, + }); + this.privateApiDefinitionSourceS3 = new S3Client({ + ...(config.privateApiDefinitionSourceS3.urlOverride != null + ? { endpoint: config.privateApiDefinitionSourceS3.urlOverride } + : {}), + region: config.privateApiDefinitionSourceS3.bucketRegion, + credentials: { + accessKeyId: config.awsAccessKey, + secretAccessKey: config.awsSecretKey, + }, + }); + } - return FdrAPI.Url(`${this.publicDocsCDNUrl}/${key}`); + async getPresignedDocsAssetsDownloadUrl({ + key, + isPrivate, + }: { + key: string; + isPrivate: boolean; + }): Promise { + if (isPrivate) { + // presigned url for private + const cachedUrl = this.presignedDownloadUrlCache.get(key); + if (cachedUrl != null && typeof cachedUrl === "string") { + return FdrAPI.Url(cachedUrl); + } + const command = new GetObjectCommand({ + Bucket: this.config.privateDocsS3.bucketName, + Key: key, + }); + const signedUrl = await getSignedUrl(this.privateDocsS3, command, { + expiresIn: 604800, + }); + this.presignedDownloadUrlCache.set(key, signedUrl); + return FdrAPI.Url(signedUrl); } - async getPresignedDocsAssetsUploadUrls({ - domain, - filepaths, - images, - isPrivate, - }: { - domain: string; - filepaths: DocsV1Write.FilePath[]; - images: DocsV2Write.ImageFilePath[]; - isPrivate: boolean; - }): Promise> { - const result: Record = {}; - const time: string = new Date().toISOString(); - for (const filepath of filepaths) { - const { url, key } = await this.createPresignedDocsAssetsUploadUrlWithClient({ - domain, - time, - filepath, - isPrivate, - }); - result[filepath] = { - presignedUrl: { - fileId: APIV1Write.FileId(uuidv4()), - uploadUrl: url, - }, - key, - imageMetadata: undefined, - }; - } - for (const image of images) { - const { url, key } = await this.createPresignedDocsAssetsUploadUrlWithClient({ - domain, - time, - filepath: image.filePath, - isPrivate, - }); - result[image.filePath] = { - presignedUrl: { - fileId: APIV1Write.FileId(uuidv4()), - uploadUrl: url, - }, - key, - imageMetadata: { - width: image.width, - height: image.height, - blurDataUrl: image.blurDataUrl, - alt: image.alt, - }, - }; - } - return result; - } + return FdrAPI.Url(`${this.publicDocsCDNUrl}/${key}`); + } - async createPresignedDocsAssetsUploadUrlWithClient({ - domain, - time, - filepath, - isPrivate, - }: { - domain: string; - time: string; - filepath: DocsV1Write.FilePath; - isPrivate: boolean; - }): Promise<{ url: string; key: string }> { - const key = this.constructS3DocsKey({ domain, time, filepath }); - const bucketName = isPrivate ? this.config.privateDocsS3.bucketName : this.config.publicDocsS3.bucketName; - const input: PutObjectCommandInput = { - Bucket: bucketName, - Key: key, - }; - if (filepath.endsWith(".svg")) { - input.ContentType = "image/svg+xml"; - } - const command = new PutObjectCommand(input); - return { - url: await getSignedUrl(isPrivate ? this.privateDocsS3 : this.publicDocsS3, command, { expiresIn: 3600 }), - key, - }; + async getPresignedDocsAssetsUploadUrls({ + domain, + filepaths, + images, + isPrivate, + }: { + domain: string; + filepaths: DocsV1Write.FilePath[]; + images: DocsV2Write.ImageFilePath[]; + isPrivate: boolean; + }): Promise> { + const result: Record = {}; + const time: string = new Date().toISOString(); + for (const filepath of filepaths) { + const { url, key } = + await this.createPresignedDocsAssetsUploadUrlWithClient({ + domain, + time, + filepath, + isPrivate, + }); + result[filepath] = { + presignedUrl: { + fileId: APIV1Write.FileId(uuidv4()), + uploadUrl: url, + }, + key, + imageMetadata: undefined, + }; } - - async getPresignedApiDefinitionSourceDownloadUrl({ key }: { key: string }): Promise { - const command = new GetObjectCommand({ - Bucket: this.config.privateApiDefinitionSourceS3.bucketName, - Key: key, + for (const image of images) { + const { url, key } = + await this.createPresignedDocsAssetsUploadUrlWithClient({ + domain, + time, + filepath: image.filePath, + isPrivate, }); - return await getSignedUrl(this.privateDocsS3, command, { expiresIn: 604800 }); + result[image.filePath] = { + presignedUrl: { + fileId: APIV1Write.FileId(uuidv4()), + uploadUrl: url, + }, + key, + imageMetadata: { + width: image.width, + height: image.height, + blurDataUrl: image.blurDataUrl, + alt: image.alt, + }, + }; } + return result; + } - async getPresignedApiDefinitionSourceUploadUrls({ - orgId, - apiId, - sources, - }: { - orgId: FernRegistry.OrgId; - apiId: FernRegistry.ApiId; - sources: Record; - }): Promise> { - const result: Record = {}; - const time: string = new Date().toISOString(); - for (const [sourceId, _source] of Object.entries(sources)) { - const { url, key } = await this.createPresignedApiDefinitionSourceUploadUrlWithClient({ - orgId, - apiId, - time, - sourceId: APIV1Write.SourceId(sourceId), - }); - result[APIV1Write.SourceId(sourceId)] = { - presignedUrl: url, - key, - }; - } - return result; + async createPresignedDocsAssetsUploadUrlWithClient({ + domain, + time, + filepath, + isPrivate, + }: { + domain: string; + time: string; + filepath: DocsV1Write.FilePath; + isPrivate: boolean; + }): Promise<{ url: string; key: string }> { + const key = this.constructS3DocsKey({ domain, time, filepath }); + const bucketName = isPrivate + ? this.config.privateDocsS3.bucketName + : this.config.publicDocsS3.bucketName; + const input: PutObjectCommandInput = { + Bucket: bucketName, + Key: key, + }; + if (filepath.endsWith(".svg")) { + input.ContentType = "image/svg+xml"; } + const command = new PutObjectCommand(input); + return { + url: await getSignedUrl( + isPrivate ? this.privateDocsS3 : this.publicDocsS3, + command, + { expiresIn: 3600 } + ), + key, + }; + } - async createPresignedApiDefinitionSourceUploadUrlWithClient({ - orgId, - apiId, - time, - sourceId, - }: { - orgId: FernRegistry.OrgId; - apiId: FernRegistry.ApiId; - time: string; - sourceId: APIV1Write.SourceId; - }): Promise<{ url: string; key: string }> { - const key = this.constructS3ApiDefinitionSourceKey({ orgId, apiId, time, sourceId }); - const bucketName = this.config.privateApiDefinitionSourceS3.bucketName; - const input: PutObjectCommandInput = { - Bucket: bucketName, - Key: key, - }; - const command = new PutObjectCommand(input); - return { - url: await getSignedUrl(this.privateApiDefinitionSourceS3, command, { expiresIn: 3600 }), - key, - }; - } + async getPresignedApiDefinitionSourceDownloadUrl({ + key, + }: { + key: string; + }): Promise { + const command = new GetObjectCommand({ + Bucket: this.config.privateApiDefinitionSourceS3.bucketName, + Key: key, + }); + return await getSignedUrl(this.privateDocsS3, command, { + expiresIn: 604800, + }); + } - constructS3DocsKey({ - domain, - time, - filepath, - }: { - domain: string; - time: string; - filepath: DocsV1Write.FilePath; - }): string { - return `${domain}/${time}/${filepath}`; + async getPresignedApiDefinitionSourceUploadUrls({ + orgId, + apiId, + sources, + }: { + orgId: FernRegistry.OrgId; + apiId: FernRegistry.ApiId; + sources: Record; + }): Promise> { + const result: Record = + {}; + const time: string = new Date().toISOString(); + for (const [sourceId, _source] of Object.entries(sources)) { + const { url, key } = + await this.createPresignedApiDefinitionSourceUploadUrlWithClient({ + orgId, + apiId, + time, + sourceId: APIV1Write.SourceId(sourceId), + }); + result[APIV1Write.SourceId(sourceId)] = { + presignedUrl: url, + key, + }; } + return result; + } - constructS3ApiDefinitionSourceKey({ - orgId, - apiId, - time, - sourceId, - }: { - orgId: FernRegistry.OrgId; - apiId: FernRegistry.ApiId; - time: string; - sourceId: APIV1Write.SourceId; - }): string { - return `${orgId}/${apiId}/${time}/${sourceId}`; - } + async createPresignedApiDefinitionSourceUploadUrlWithClient({ + orgId, + apiId, + time, + sourceId, + }: { + orgId: FernRegistry.OrgId; + apiId: FernRegistry.ApiId; + time: string; + sourceId: APIV1Write.SourceId; + }): Promise<{ url: string; key: string }> { + const key = this.constructS3ApiDefinitionSourceKey({ + orgId, + apiId, + time, + sourceId, + }); + const bucketName = this.config.privateApiDefinitionSourceS3.bucketName; + const input: PutObjectCommandInput = { + Bucket: bucketName, + Key: key, + }; + const command = new PutObjectCommand(input); + return { + url: await getSignedUrl(this.privateApiDefinitionSourceS3, command, { + expiresIn: 3600, + }), + key, + }; + } + + constructS3DocsKey({ + domain, + time, + filepath, + }: { + domain: string; + time: string; + filepath: DocsV1Write.FilePath; + }): string { + return `${domain}/${time}/${filepath}`; + } + + constructS3ApiDefinitionSourceKey({ + orgId, + apiId, + time, + sourceId, + }: { + orgId: FernRegistry.OrgId; + apiId: FernRegistry.ApiId; + time: string; + sourceId: APIV1Write.SourceId; + }): string { + return `${orgId}/${apiId}/${time}/${sourceId}`; + } } diff --git a/servers/fdr/src/services/s3/__test__/s3.test.ts b/servers/fdr/src/services/s3/__test__/s3.test.ts index c2609891ac..fde0ed913b 100644 --- a/servers/fdr/src/services/s3/__test__/s3.test.ts +++ b/servers/fdr/src/services/s3/__test__/s3.test.ts @@ -1,4 +1,8 @@ -import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { + GetObjectCommand, + PutObjectCommand, + S3Client, +} from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import fs from "fs"; import { readFile } from "fs/promises"; @@ -15,78 +19,87 @@ const AWS_SECRET_KEY = "AWS_SECRET_KEY"; const AWS_BUCKET_NAME = "AWS_BUCKET_NAME"; it.skip("presigned URLs", async () => { - const client = new S3Client({ - region: "us-east-1", - credentials: { - accessKeyId: AWS_ACCESS_KEY, - secretAccessKey: AWS_SECRET_KEY, - }, - }); - - const time: string = new Date().toISOString(); - const sourceId: string = uuidv4(); - - const putCommand = new PutObjectCommand({ - Bucket: AWS_BUCKET_NAME, - Key: `fern/fern/${time}/${sourceId}`, - }); - - const uploadUrl = await getSignedUrl(client, putCommand, { expiresIn: 3600 }); - expect(uploadUrl).not.toBe(null); - expect(uploadUrl.length).greaterThan(0); - - console.log("Upload URL: ", uploadUrl); - - const getCommand = new GetObjectCommand({ - Bucket: AWS_BUCKET_NAME, - Key: `fern/fern/${time}/${sourceId}`, - }); - const downloadUrl = await getSignedUrl(client, getCommand, { expiresIn: 604800 }); - expect(downloadUrl).not.toBe(null); - expect(downloadUrl.length).greaterThan(0); - - console.log("Download URL: ", uploadUrl); - - console.log("Uploading zip ..."); - await uploadFile(path.join(__dirname, "proto.zip"), uploadUrl); - - console.log("Downloading zip ..."); - await downloadFile("./proto.downloaded.zip", downloadUrl); - - console.log("Success!"); + const client = new S3Client({ + region: "us-east-1", + credentials: { + accessKeyId: AWS_ACCESS_KEY, + secretAccessKey: AWS_SECRET_KEY, + }, + }); + + const time: string = new Date().toISOString(); + const sourceId: string = uuidv4(); + + const putCommand = new PutObjectCommand({ + Bucket: AWS_BUCKET_NAME, + Key: `fern/fern/${time}/${sourceId}`, + }); + + const uploadUrl = await getSignedUrl(client, putCommand, { expiresIn: 3600 }); + expect(uploadUrl).not.toBe(null); + expect(uploadUrl.length).greaterThan(0); + + console.log("Upload URL: ", uploadUrl); + + const getCommand = new GetObjectCommand({ + Bucket: AWS_BUCKET_NAME, + Key: `fern/fern/${time}/${sourceId}`, + }); + const downloadUrl = await getSignedUrl(client, getCommand, { + expiresIn: 604800, + }); + expect(downloadUrl).not.toBe(null); + expect(downloadUrl.length).greaterThan(0); + + console.log("Download URL: ", uploadUrl); + + console.log("Uploading zip ..."); + await uploadFile(path.join(__dirname, "proto.zip"), uploadUrl); + + console.log("Downloading zip ..."); + await downloadFile("./proto.downloaded.zip", downloadUrl); + + console.log("Success!"); }, 100_000); async function uploadFile(filePath: string, uploadUrl: string): Promise { - try { - const fileData = await readFile(filePath); - const response = await fetch(uploadUrl, { - method: "PUT", - body: fileData, - headers: { - "Content-Type": "application/octet-stream", - }, - }); - if (response.ok) { - console.log("File uploaded successfully!"); - } else { - console.error(`Failed to upload file. Status: ${response.status}, ${response.statusText}`); - } - } catch (error) { - console.error("Error uploading file:", error); + try { + const fileData = await readFile(filePath); + const response = await fetch(uploadUrl, { + method: "PUT", + body: fileData, + headers: { + "Content-Type": "application/octet-stream", + }, + }); + if (response.ok) { + console.log("File uploaded successfully!"); + } else { + console.error( + `Failed to upload file. Status: ${response.status}, ${response.statusText}` + ); } + } catch (error) { + console.error("Error uploading file:", error); + } } -async function downloadFile(savePath: string, downloadUrl: string): Promise { - try { - const response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error(`Failed to download file. Status: ${response.status}, ${response.statusText}`); - } - const fileStream = fs.createWriteStream(savePath); - await streamPipeline(response.body as any, fileStream); - - console.log("File downloaded successfully!"); - } catch (error) { - console.error("Error downloading file:", error); +async function downloadFile( + savePath: string, + downloadUrl: string +): Promise { + try { + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error( + `Failed to download file. Status: ${response.status}, ${response.statusText}` + ); } + const fileStream = fs.createWriteStream(savePath); + await streamPipeline(response.body as any, fileStream); + + console.log("File downloaded successfully!"); + } catch (error) { + console.error("Error downloading file:", error); + } } diff --git a/servers/fdr/src/services/s3/index.ts b/servers/fdr/src/services/s3/index.ts index 89ebd96952..53353f685c 100644 --- a/servers/fdr/src/services/s3/index.ts +++ b/servers/fdr/src/services/s3/index.ts @@ -1 +1,5 @@ -export { S3ServiceImpl, type S3DocsFileInfo, type S3Service } from "./S3Service"; +export { + S3ServiceImpl, + type S3DocsFileInfo, + type S3Service, +} from "./S3Service"; diff --git a/servers/fdr/src/services/slack/SlackService.ts b/servers/fdr/src/services/slack/SlackService.ts index 602f54dceb..7161ecfca6 100644 --- a/servers/fdr/src/services/slack/SlackService.ts +++ b/servers/fdr/src/services/slack/SlackService.ts @@ -4,134 +4,148 @@ import type { FdrApplication, FdrConfig } from "../../app"; import { RevalidatedPathsResponse } from "../revalidator/RevalidatorService"; export interface FailedToRegisterDocsNotification { - domain: string; - err: unknown; + domain: string; + err: unknown; } export interface FailedToRevalidatePathsNotification { - domain: string; - paths: RevalidatedPathsResponse; + domain: string; + paths: RevalidatedPathsResponse; } export interface FailedToDeleteIndexSegment { - indexSegmentId: string; - err: unknown; + indexSegmentId: string; + err: unknown; } export interface GeneratingDocsNotification { - orgId: string; - urls: string[]; + orgId: string; + urls: string[]; } export interface SlackService { - notifyGeneratedDocs(request: GeneratingDocsNotification): Promise; - notifyFailedToRegisterDocs(request: FailedToRegisterDocsNotification): Promise; - notifyFailedToRevalidatePaths(request: FailedToRevalidatePathsNotification): Promise; - notifyFailedToDeleteIndexSegment(request: FailedToDeleteIndexSegment): Promise; - notify(message: string, err: unknown): Promise; + notifyGeneratedDocs(request: GeneratingDocsNotification): Promise; + notifyFailedToRegisterDocs( + request: FailedToRegisterDocsNotification + ): Promise; + notifyFailedToRevalidatePaths( + request: FailedToRevalidatePathsNotification + ): Promise; + notifyFailedToDeleteIndexSegment( + request: FailedToDeleteIndexSegment + ): Promise; + notify(message: string, err: unknown): Promise; } export class SlackServiceImpl implements SlackService { - private client: WebClient; - private logger: winston.Logger; - private config: FdrConfig; + private client: WebClient; + private logger: winston.Logger; + private config: FdrConfig; - constructor(app: FdrApplication) { - this.config = app.config; - this.client = new WebClient(this.config.slackToken); - this.logger = app.logger; - } + constructor(app: FdrApplication) { + this.config = app.config; + this.client = new WebClient(this.config.slackToken); + this.logger = app.logger; + } - async notifyGeneratedDocs(request: GeneratingDocsNotification): Promise { - if (this.config.enableCustomerNotifications) { - try { - await this.client.chat.postMessage({ - channel: "#customer-notifs", - text: `:herb: ${request.orgId} is generating docs for urls [${request.urls.join(", ")}]`, - blocks: [], - }); - } catch (err) { - this.logger.debug("Failed to send slack message: ", err); - } - } + async notifyGeneratedDocs( + request: GeneratingDocsNotification + ): Promise { + if (this.config.enableCustomerNotifications) { + try { + await this.client.chat.postMessage({ + channel: "#customer-notifs", + text: `:herb: ${request.orgId} is generating docs for urls [${request.urls.join(", ")}]`, + blocks: [], + }); + } catch (err) { + this.logger.debug("Failed to send slack message: ", err); + } } + } - async notify(message: string, err: unknown): Promise { - try { - await this.client.chat.postMessage({ - channel: "#activity", - text: `:rotating_light: Encountered failure in FDR: ${message}.\n ${stringifyError(err)}`, - blocks: [], - }); - } catch (err) { - this.logger.debug("Failed to send slack message: ", err); - } + async notify(message: string, err: unknown): Promise { + try { + await this.client.chat.postMessage({ + channel: "#activity", + text: `:rotating_light: Encountered failure in FDR: ${message}.\n ${stringifyError(err)}`, + blocks: [], + }); + } catch (err) { + this.logger.debug("Failed to send slack message: ", err); } + } - async notifyFailedToRevalidatePaths(request: FailedToRevalidatePathsNotification): Promise { - try { - const failedRevalidations = request.paths.failed; - if (request.paths.failed.length > 0) { - const { ts } = await this.client.chat.postMessage({ - channel: "#engineering-notifs", - text: `:rotating_light: \`${request.domain}\` encountered ${failedRevalidations.length} revalidation failurs. }`, - blocks: [], - }); - const failedUrlsMessage = `The following paths failed:\n ${failedRevalidations - .map((e) => `${(e as any).url} : ${(e as any).message}`) - .join("\n")}`; - await this.client.chat.postMessage({ - channel: "#engineering-notifs", - text: failedUrlsMessage, - thread_ts: ts, - }); - } else if (request.paths.revalidationFailed) { - await this.client.chat.postMessage({ - channel: "#engineering-notifs", - text: `:rotating_light: \`${request.domain}\` revalidation *completely* failed.`, - blocks: [], - }); - } - } catch (err) { - this.logger.debug("Failed to send slack message: ", err); - } + async notifyFailedToRevalidatePaths( + request: FailedToRevalidatePathsNotification + ): Promise { + try { + const failedRevalidations = request.paths.failed; + if (request.paths.failed.length > 0) { + const { ts } = await this.client.chat.postMessage({ + channel: "#engineering-notifs", + text: `:rotating_light: \`${request.domain}\` encountered ${failedRevalidations.length} revalidation failurs. }`, + blocks: [], + }); + const failedUrlsMessage = `The following paths failed:\n ${failedRevalidations + .map((e) => `${(e as any).url} : ${(e as any).message}`) + .join("\n")}`; + await this.client.chat.postMessage({ + channel: "#engineering-notifs", + text: failedUrlsMessage, + thread_ts: ts, + }); + } else if (request.paths.revalidationFailed) { + await this.client.chat.postMessage({ + channel: "#engineering-notifs", + text: `:rotating_light: \`${request.domain}\` revalidation *completely* failed.`, + blocks: [], + }); + } + } catch (err) { + this.logger.debug("Failed to send slack message: ", err); } + } - public async notifyFailedToRegisterDocs(request: FailedToRegisterDocsNotification): Promise { - try { - await this.client.chat.postMessage({ - channel: "#engineering-notifs", - text: `:rotating_light: Docs failed to register \`${request.domain}\`: ${stringifyError(request.err)}`, - blocks: [], - }); - } catch (err) { - this.logger.debug("Failed to send slack message: ", err); - } + public async notifyFailedToRegisterDocs( + request: FailedToRegisterDocsNotification + ): Promise { + try { + await this.client.chat.postMessage({ + channel: "#engineering-notifs", + text: `:rotating_light: Docs failed to register \`${request.domain}\`: ${stringifyError(request.err)}`, + blocks: [], + }); + } catch (err) { + this.logger.debug("Failed to send slack message: ", err); } + } - public async notifyFailedToDeleteIndexSegment(request: FailedToDeleteIndexSegment): Promise { - const { indexSegmentId, err } = request; + public async notifyFailedToDeleteIndexSegment( + request: FailedToDeleteIndexSegment + ): Promise { + const { indexSegmentId, err } = request; - try { - await this.client.chat.postMessage({ - channel: "#engineering-notifs", - text: `:rotating_light: Failed to delete index segment \`${indexSegmentId}\`: ${stringifyError(err)}`, - blocks: [], - }); - } catch (err) { - this.logger.debug("Failed to send slack message: ", err); - } + try { + await this.client.chat.postMessage({ + channel: "#engineering-notifs", + text: `:rotating_light: Failed to delete index segment \`${indexSegmentId}\`: ${stringifyError(err)}`, + blocks: [], + }); + } catch (err) { + this.logger.debug("Failed to send slack message: ", err); } + } } function stringifyError(error: unknown): string { - if (error instanceof Error) { - const message = error.message; // Get the error message - const stackTrace = error.stack; // Get the stack trace - return `Error Message: ${message}\nStack Trace:\n${stackTrace}`; - } else if (typeof error === "string") { - return error; // If error is a string, just return it as is - } else { - return "An unknown error occurred"; - } + if (error instanceof Error) { + const message = error.message; // Get the error message + const stackTrace = error.stack; // Get the stack trace + return `Error Message: ${message}\nStack Trace:\n${stackTrace}`; + } else if (typeof error === "string") { + return error; // If error is a string, just return it as is + } else { + return "An unknown error occurred"; + } } diff --git a/servers/fdr/src/types.ts b/servers/fdr/src/types.ts index c16c6962de..27b8078660 100644 --- a/servers/fdr/src/types.ts +++ b/servers/fdr/src/types.ts @@ -1,6 +1,6 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; export interface DocsVersion { - id: FdrAPI.VersionId; - urlSlug?: string; + id: FdrAPI.VersionId; + urlSlug?: string; } diff --git a/servers/fdr/src/util/ParsedBaseUrl.ts b/servers/fdr/src/util/ParsedBaseUrl.ts index 8239bdc544..7e2e923d74 100644 --- a/servers/fdr/src/util/ParsedBaseUrl.ts +++ b/servers/fdr/src/util/ParsedBaseUrl.ts @@ -1,38 +1,49 @@ const HAS_HTTPS_REGEX = /^https?:\/\//i; export class ParsedBaseUrl { - public readonly hostname: string; - public readonly path: string | undefined; + public readonly hostname: string; + public readonly path: string | undefined; - private constructor({ hostname, path }: { hostname: string; path: string | undefined }) { - this.hostname = hostname; - this.path = path; - } + private constructor({ + hostname, + path, + }: { + hostname: string; + path: string | undefined; + }) { + this.hostname = hostname; + this.path = path; + } - public getFullUrl(): string { - if (this.path == null) { - return this.hostname; - } - return `${this.hostname}${this.path}`; + public getFullUrl(): string { + if (this.path == null) { + return this.hostname; } + return `${this.hostname}${this.path}`; + } - public toURL(): URL { - return new URL(`https://${this.getFullUrl()}`); - } + public toURL(): URL { + return new URL(`https://${this.getFullUrl()}`); + } - public static parse(url: string): ParsedBaseUrl { - try { - let urlWithHttpsPrefix = url; - if (!HAS_HTTPS_REGEX.test(url)) { - urlWithHttpsPrefix = "https://" + url; - } - const parsedURL = new URL(urlWithHttpsPrefix); - return new ParsedBaseUrl({ - hostname: parsedURL.hostname, - path: parsedURL.pathname === "/" || parsedURL.pathname === "" ? undefined : parsedURL.pathname, - }); - } catch (e) { - throw new Error(`Failed to parse URL: ${url}. The error was ${(e as Error)?.message}`); - } + public static parse(url: string): ParsedBaseUrl { + try { + let urlWithHttpsPrefix = url; + if (!HAS_HTTPS_REGEX.test(url)) { + urlWithHttpsPrefix = "https://" + url; + } + const parsedURL = new URL(urlWithHttpsPrefix); + return new ParsedBaseUrl({ + hostname: parsedURL.hostname, + path: + parsedURL.pathname === "/" || parsedURL.pathname === "" + ? undefined + : parsedURL.pathname, + }); + } catch (e) { + throw new Error( + `Failed to parse URL: ${url}. The error was ${(e as Error)?.message}` + ); } + } } diff --git a/servers/fdr/src/util/WithoutQuestionMarks.ts b/servers/fdr/src/util/WithoutQuestionMarks.ts index 1cb9d0f26e..6caf97564f 100644 --- a/servers/fdr/src/util/WithoutQuestionMarks.ts +++ b/servers/fdr/src/util/WithoutQuestionMarks.ts @@ -1,3 +1,3 @@ export type WithoutQuestionMarks = { - [K in keyof Required]: undefined extends T[K] ? T[K] | undefined : T[K]; + [K in keyof Required]: undefined extends T[K] ? T[K] | undefined : T[K]; }; diff --git a/servers/fdr/src/util/assertNever.ts b/servers/fdr/src/util/assertNever.ts index e96382acf1..0703f1d4ee 100644 --- a/servers/fdr/src/util/assertNever.ts +++ b/servers/fdr/src/util/assertNever.ts @@ -1,5 +1,5 @@ export function assertNever(x: never): never { - throw new Error("Unexpected value: " + JSON.stringify(x)); + throw new Error("Unexpected value: " + JSON.stringify(x)); } // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/servers/fdr/src/util/bytes.ts b/servers/fdr/src/util/bytes.ts index 6c3c55fc9f..a49fdff8d8 100644 --- a/servers/fdr/src/util/bytes.ts +++ b/servers/fdr/src/util/bytes.ts @@ -1,40 +1,47 @@ -function _truncate(getLength: (str: string) => number, string: string, byteLength: number) { - if (typeof string !== "string") { - throw new Error("Input must be string"); +function _truncate( + getLength: (str: string) => number, + string: string, + byteLength: number +) { + if (typeof string !== "string") { + throw new Error("Input must be string"); + } + + let curByteLength = 0; + let codePoint; + let segment; + + for (let i = 0; i < string.length; i += 1) { + codePoint = string.charCodeAt(i); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + segment = string[i]!; + + if ( + isHighSurrogate(codePoint) && + isLowSurrogate(string.charCodeAt(i + 1)) + ) { + i += 1; + segment += string[i]; } - let curByteLength = 0; - let codePoint; - let segment; + curByteLength += getLength(segment); - for (let i = 0; i < string.length; i += 1) { - codePoint = string.charCodeAt(i); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - segment = string[i]!; - - if (isHighSurrogate(codePoint) && isLowSurrogate(string.charCodeAt(i + 1))) { - i += 1; - segment += string[i]; - } - - curByteLength += getLength(segment); - - if (curByteLength === byteLength) { - return string.slice(0, i + 1); - } else if (curByteLength > byteLength) { - return string.slice(0, i - segment.length + 1); - } + if (curByteLength === byteLength) { + return string.slice(0, i + 1); + } else if (curByteLength > byteLength) { + return string.slice(0, i - segment.length + 1); } + } - return string; + return string; } function isHighSurrogate(codePoint: number) { - return codePoint >= 0xd800 && codePoint <= 0xdbff; + return codePoint >= 0xd800 && codePoint <= 0xdbff; } function isLowSurrogate(codePoint: number) { - return codePoint >= 0xdc00 && codePoint <= 0xdfff; + return codePoint >= 0xdc00 && codePoint <= 0xdfff; } /** @@ -42,4 +49,7 @@ function isLowSurrogate(codePoint: number) { * * @see https://github.com/parshap/truncate-utf8-bytes */ -export const truncateToBytes = _truncate.bind(null, Buffer.byteLength.bind(Buffer)); +export const truncateToBytes = _truncate.bind( + null, + Buffer.byteLength.bind(Buffer) +); diff --git a/servers/fdr/src/util/getFilesV2.ts b/servers/fdr/src/util/getFilesV2.ts index a746c86f50..6d21cfdfa8 100644 --- a/servers/fdr/src/util/getFilesV2.ts +++ b/servers/fdr/src/util/getFilesV2.ts @@ -1,39 +1,48 @@ import { DocsV1Db, DocsV1Read } from "@fern-api/fdr-sdk"; import { FdrApplication } from "../app"; -export async function getFilesV2(docsDbDefinition: DocsV1Db.DocsDefinitionDb, app: FdrApplication) { - let promisedFiles: Promise<[DocsV1Read.FileId, DocsV1Read.File_]>[]; - if (docsDbDefinition.type === "v3") { - promisedFiles = Object.entries(docsDbDefinition.files).map( - async ([fileId, fileDbInfo]): Promise<[DocsV1Read.FileId, DocsV1Read.File_]> => { - const s3DownloadUrl = await app.services.s3.getPresignedDocsAssetsDownloadUrl({ - key: fileDbInfo.s3Key, - isPrivate: true, // for backcompat - }); - const readFile: DocsV1Read.File_ = - fileDbInfo.type === "image" - ? { - type: "image", - url: s3DownloadUrl, - width: fileDbInfo.width, - height: fileDbInfo.height, - blurDataUrl: fileDbInfo.blurDataUrl, - alt: fileDbInfo.alt, - } - : { type: "url", url: s3DownloadUrl }; - return [DocsV1Read.FileId(fileId), readFile]; - }, - ); - } else { - promisedFiles = Object.entries(docsDbDefinition.files).map( - async ([fileId, fileDbInfo]): Promise<[DocsV1Read.FileId, DocsV1Read.File_]> => { - const s3DownloadUrl = await app.services.s3.getPresignedDocsAssetsDownloadUrl({ - key: fileDbInfo.s3Key, - isPrivate: true, // for backcompat - }); - return [DocsV1Read.FileId(fileId), { type: "url", url: s3DownloadUrl }]; - }, - ); - } - return Object.fromEntries(await Promise.all(promisedFiles)); +export async function getFilesV2( + docsDbDefinition: DocsV1Db.DocsDefinitionDb, + app: FdrApplication +) { + let promisedFiles: Promise<[DocsV1Read.FileId, DocsV1Read.File_]>[]; + if (docsDbDefinition.type === "v3") { + promisedFiles = Object.entries(docsDbDefinition.files).map( + async ([fileId, fileDbInfo]): Promise< + [DocsV1Read.FileId, DocsV1Read.File_] + > => { + const s3DownloadUrl = + await app.services.s3.getPresignedDocsAssetsDownloadUrl({ + key: fileDbInfo.s3Key, + isPrivate: true, // for backcompat + }); + const readFile: DocsV1Read.File_ = + fileDbInfo.type === "image" + ? { + type: "image", + url: s3DownloadUrl, + width: fileDbInfo.width, + height: fileDbInfo.height, + blurDataUrl: fileDbInfo.blurDataUrl, + alt: fileDbInfo.alt, + } + : { type: "url", url: s3DownloadUrl }; + return [DocsV1Read.FileId(fileId), readFile]; + } + ); + } else { + promisedFiles = Object.entries(docsDbDefinition.files).map( + async ([fileId, fileDbInfo]): Promise< + [DocsV1Read.FileId, DocsV1Read.File_] + > => { + const s3DownloadUrl = + await app.services.s3.getPresignedDocsAssetsDownloadUrl({ + key: fileDbInfo.s3Key, + isPrivate: true, // for backcompat + }); + return [DocsV1Read.FileId(fileId), { type: "url", url: s3DownloadUrl }]; + } + ); + } + return Object.fromEntries(await Promise.all(promisedFiles)); } diff --git a/servers/fdr/src/util/markdown.ts b/servers/fdr/src/util/markdown.ts index 85c89aae89..0c9c6e4257 100644 --- a/servers/fdr/src/util/markdown.ts +++ b/servers/fdr/src/util/markdown.ts @@ -4,6 +4,6 @@ import { marked } from "marked"; const convertHtmlToText = compile({ wordwrap: 130 }); export function convertMarkdownToText(md: string) { - const htmlStr = marked(md, { mangle: false, headerIds: false }); - return convertHtmlToText(htmlStr); + const htmlStr = marked(md, { mangle: false, headerIds: false }); + return convertHtmlToText(htmlStr); } diff --git a/servers/fdr/src/util/object.ts b/servers/fdr/src/util/object.ts index f658ea0a4d..9571509035 100644 --- a/servers/fdr/src/util/object.ts +++ b/servers/fdr/src/util/object.ts @@ -1,35 +1,39 @@ import { isPlainObject } from "es-toolkit/predicate"; -export const isPlainObject2 = isPlainObject as (val: unknown) => val is Record; +export const isPlainObject2 = isPlainObject as ( + val: unknown +) => val is Record; /** * Deep clones the specified object omitting keys with `undefined` value. */ export function compact>(source: T) { - const obj: Record = {}; - Object.keys(source).forEach((key) => { - const val = source[key]; - if (val !== undefined) { - obj[key] = compactVal(val); - } - }); - return obj as T; + const obj: Record = {}; + Object.keys(source).forEach((key) => { + const val = source[key]; + if (val !== undefined) { + obj[key] = compactVal(val); + } + }); + return obj as T; } function compactVal(val: unknown): unknown { - if (Array.isArray(val)) { - return val.filter((v) => v !== undefined).map((v) => compactVal(v)); - } - if (isPlainObject2(val)) { - return compact(val); - } - return val; + if (Array.isArray(val)) { + return val.filter((v) => v !== undefined).map((v) => compactVal(v)); + } + if (isPlainObject2(val)) { + return compact(val); + } + return val; } -export function createObjectFromMap(map: Map): Record { - const obj = {} as Record; - map.forEach((val, key) => { - obj[key] = val; - }); - return obj; +export function createObjectFromMap( + map: Map +): Record { + const obj = {} as Record; + map.forEach((val, key) => { + obj[key] = val; + }); + return obj; } diff --git a/servers/fdr/src/util/serde.ts b/servers/fdr/src/util/serde.ts index 898546f554..45bb4bb710 100644 --- a/servers/fdr/src/util/serde.ts +++ b/servers/fdr/src/util/serde.ts @@ -1,15 +1,15 @@ import { LOGGER } from "../app/FdrApplication"; export function writeBuffer(val: unknown): Buffer { - return Buffer.from(JSON.stringify(val), "utf-8"); + return Buffer.from(JSON.stringify(val), "utf-8"); } export function readBuffer(val: Buffer): unknown { - const raw = val.toString(); - try { - return JSON.parse(raw); - } catch (e) { - LOGGER.error(`Failed to parse buffer: ${raw}`); - throw e; - } + const raw = val.toString(); + try { + return JSON.parse(raw); + } catch (e) { + LOGGER.error(`Failed to parse buffer: ${raw}`); + throw e; + } } diff --git a/servers/fdr/vitest.config.ts b/servers/fdr/vitest.config.ts index 4899c39b3f..1135baab67 100644 --- a/servers/fdr/vitest.config.ts +++ b/servers/fdr/vitest.config.ts @@ -1,8 +1,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - exclude: ["src/__test__/local/**", "src/__test__/ete/**", "node_modules/**"], - }, + test: { + globals: true, + exclude: [ + "src/__test__/local/**", + "src/__test__/ete/**", + "node_modules/**", + ], + }, }); diff --git a/servers/fern-bot/.prettierrc.json b/servers/fern-bot/.prettierrc.json deleted file mode 100644 index 7c9b843095..0000000000 --- a/servers/fern-bot/.prettierrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 4, - "overrides": [ - { - "files": "*.{yml,yaml,json,md,mdx}", - "options": { - "tabWidth": 2 - } - } - ] -} diff --git a/servers/fern-bot/package.json b/servers/fern-bot/package.json index f18ada55f7..fbb084011b 100644 --- a/servers/fern-bot/package.json +++ b/servers/fern-bot/package.json @@ -1,21 +1,19 @@ { "name": "@fern-platform/fern-bot", "version": "0.0.0", + "license": "MIT", "scripts": { "compile": "tsc --build", - "package": "sls package", - "release": "sls deploy", + "format": "prettier --write --ignore-unknown \"**\"", "invoke": "sls invoke", - "test:ete": "vitest --run --passWithNoTests --globals", "lint": "eslint --max-warnings 0 src --ext .ts", - "format": "prettier --write --ignore-unknown \"**\"", + "lint:eslint": "eslint --max-warnings 0 .", + "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:fix": "pnpm lint --fix", + "package": "sls package", "proxy": "smee -u https://smee.io/3DXoSvCO2NH87w8e", - "lint:eslint": "eslint --max-warnings 0 .", - "lint:eslint:fix": "pnpm lint:eslint --fix" - }, - "engines": { - "node": ">=14.15.0" + "release": "sls deploy", + "test:ete": "vitest --run --passWithNoTests --globals" }, "dependencies": { "@aws-sdk/client-s3": "^3.685.0", @@ -49,6 +47,7 @@ "@types/aws-lambda": "^8.10.71", "@types/js-yaml": "^4.0.9", "@types/node": "^18.7.18", + "@types/semver": "^7.5.8", "@types/url-join": "4.0.1", "esbuild": "0.20.2", "json-schema-to-ts": "^1.5.0", @@ -57,8 +56,9 @@ "ts-node": "^10.4.0", "tsconfig-paths": "^3.9.0", "typescript": "^4.1.3", - "vitest": "^2.1.4", - "@types/semver": "^7.5.8" + "vitest": "^2.1.4" }, - "license": "MIT" + "engines": { + "node": ">=14.15.0" + } } diff --git a/servers/fern-bot/src/__test__/basic-ete.test.ts b/servers/fern-bot/src/__test__/basic-ete.test.ts index 8de931c0b9..61b826b461 100644 --- a/servers/fern-bot/src/__test__/basic-ete.test.ts +++ b/servers/fern-bot/src/__test__/basic-ete.test.ts @@ -15,151 +15,161 @@ const CLI_TEST_BRANCH = "fern/update/cli"; const PYTHON_TEST_BRANCH = "fern/update/fern-python-sdk@local"; const JAVA_TEST_BRANCH = "fern/update/fern-java-sdk@another-group"; -export async function getBranch(git: SimpleGit, branchToCheckoutName: string): Promise { - await git.fetch(DEFAULT_REMOTE_NAME, branchToCheckoutName); - await git.checkout(branchToCheckoutName); +export async function getBranch( + git: SimpleGit, + branchToCheckoutName: string +): Promise { + await git.fetch(DEFAULT_REMOTE_NAME, branchToCheckoutName); + await git.checkout(branchToCheckoutName); } beforeEach(async () => { - const env = evaluateEnv(); - const app: App = setupGithubApp(env); - await app.eachRepository(async (installation) => { - if (installation.repository.full_name !== REPO_FULL_NAME) { - return; - } + const env = evaluateEnv(); + const app: App = setupGithubApp(env); + await app.eachRepository(async (installation) => { + if (installation.repository.full_name !== REPO_FULL_NAME) { + return; + } - const [git, _] = await configureGit(installation.repository); - // Delete the branches, if they exist - await cloneRepo( - git, - installation.repository, - installation.octokit, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - ); - try { - await installation.octokit.rest.git.deleteRef({ - owner: installation.repository.owner.login, - repo: installation.repository.name, - ref: `heads/${CLI_TEST_BRANCH}`, - }); - await installation.octokit.rest.git.deleteRef({ - owner: installation.repository.owner.login, - repo: installation.repository.name, - ref: `heads/${PYTHON_TEST_BRANCH}`, - }); - await installation.octokit.rest.git.deleteRef({ - owner: installation.repository.owner.login, - repo: installation.repository.name, - ref: `heads/${JAVA_TEST_BRANCH}`, - }); - } catch (e) { - console.log("Branches do not exist, continuing, consider `beforeEach` a success.", e); - } - }); + const [git, _] = await configureGit(installation.repository); + // Delete the branches, if they exist + await cloneRepo( + git, + installation.repository, + installation.octokit, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID + ); + try { + await installation.octokit.rest.git.deleteRef({ + owner: installation.repository.owner.login, + repo: installation.repository.name, + ref: `heads/${CLI_TEST_BRANCH}`, + }); + await installation.octokit.rest.git.deleteRef({ + owner: installation.repository.owner.login, + repo: installation.repository.name, + ref: `heads/${PYTHON_TEST_BRANCH}`, + }); + await installation.octokit.rest.git.deleteRef({ + owner: installation.repository.owner.login, + repo: installation.repository.name, + ref: `heads/${JAVA_TEST_BRANCH}`, + }); + } catch (e) { + console.log( + "Branches do not exist, continuing, consider `beforeEach` a success.", + e + ); + } + }); }); it( - "happy path fern-bot upgrade", - async () => { - const env = evaluateEnv(); - const app: App = setupGithubApp(env); - await app.eachRepository(async (installation) => { - if (installation.repository.full_name !== REPO_FULL_NAME) { - return; - } + "happy path fern-bot upgrade", + async () => { + const env = evaluateEnv(); + const app: App = setupGithubApp(env); + await app.eachRepository(async (installation) => { + if (installation.repository.full_name !== REPO_FULL_NAME) { + return; + } - const [git, fullRepoPath] = await configureGit(installation.repository); - // Delete the branches - await cloneRepo( - git, - installation.repository, - installation.octokit, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - ); + const [git, fullRepoPath] = await configureGit(installation.repository); + // Delete the branches + await cloneRepo( + git, + installation.repository, + installation.octokit, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID + ); - // Get versions off main - await git.checkout("main"); - const cliVersion = cleanStdout((await execFernCli("--version", fullRepoPath)).stdout); - const pythonVersion = cleanStdout( - ( - await execFernCli( - "generator get --version --generator fernapi/fern-python-sdk --group local", - fullRepoPath, - ) - ).stdout, - ); - const anotherGroupPythonVersion = cleanStdout( - ( - await execFernCli( - "generator get --version --generator fernapi/fern-python-sdk --group another-group", - fullRepoPath, - ) - ).stdout, - ); - const javaVersion = cleanStdout( - ( - await execFernCli( - "generator get --version --generator fernapi/fern-java-sdk --group another-group", - fullRepoPath, - ) - ).stdout, - ); + // Get versions off main + await git.checkout("main"); + const cliVersion = cleanStdout( + (await execFernCli("--version", fullRepoPath)).stdout + ); + const pythonVersion = cleanStdout( + ( + await execFernCli( + "generator get --version --generator fernapi/fern-python-sdk --group local", + fullRepoPath + ) + ).stdout + ); + const anotherGroupPythonVersion = cleanStdout( + ( + await execFernCli( + "generator get --version --generator fernapi/fern-python-sdk --group another-group", + fullRepoPath + ) + ).stdout + ); + const javaVersion = cleanStdout( + ( + await execFernCli( + "generator get --version --generator fernapi/fern-java-sdk --group another-group", + fullRepoPath + ) + ).stdout + ); - // Run local invoke of the bot - // TODO: it'd be great if this could be called directly to more appropriately mirror the lambda, - // but execa continues to have difficulties running pnpm. - // - // Instead we just invoke the function directly - process.env.REPO_TO_RUN_ON = REPO_FULL_NAME; - await updateGeneratorVersions({}); + // Run local invoke of the bot + // TODO: it'd be great if this could be called directly to more appropriately mirror the lambda, + // but execa continues to have difficulties running pnpm. + // + // Instead we just invoke the function directly + process.env.REPO_TO_RUN_ON = REPO_FULL_NAME; + await updateGeneratorVersions({}); - try { - // Pull each branch and make sure the version is not what it was (hardcoded) - await getBranch(git, CLI_TEST_BRANCH); - const upgradedCliVersion = cleanStdout((await execFernCli("--version", fullRepoPath)).stdout); - expect(upgradedCliVersion).not.toBe(cliVersion); - } catch (e) { - console.log( - "Error in CLI branch, likely because main has been updated to latest, so no upgrade branch has been made.", - ); - console.log(e); - } + try { + // Pull each branch and make sure the version is not what it was (hardcoded) + await getBranch(git, CLI_TEST_BRANCH); + const upgradedCliVersion = cleanStdout( + (await execFernCli("--version", fullRepoPath)).stdout + ); + expect(upgradedCliVersion).not.toBe(cliVersion); + } catch (e) { + console.log( + "Error in CLI branch, likely because main has been updated to latest, so no upgrade branch has been made." + ); + console.log(e); + } - await getBranch(git, PYTHON_TEST_BRANCH); - const upgradedPythonVersion = cleanStdout( - ( - await execFernCli( - "generator get --group local --generator fernapi/fern-python-sdk --version", - fullRepoPath, - ) - ).stdout, - ); - expect(upgradedPythonVersion).not.toBe(pythonVersion); + await getBranch(git, PYTHON_TEST_BRANCH); + const upgradedPythonVersion = cleanStdout( + ( + await execFernCli( + "generator get --group local --generator fernapi/fern-python-sdk --version", + fullRepoPath + ) + ).stdout + ); + expect(upgradedPythonVersion).not.toBe(pythonVersion); - await getBranch(git, JAVA_TEST_BRANCH); - // Make sure we're not including major version bumps in other PRs - const upgradedPythonVersionJavaBranch = cleanStdout( - ( - await execFernCli( - "generator get --group another-group --generator fernapi/fern-python-sdk --version", - fullRepoPath, - ) - ).stdout, - ); - expect(upgradedPythonVersionJavaBranch).toBe(anotherGroupPythonVersion); - const upgradedJavaVersion = cleanStdout( - ( - await execFernCli( - "generator get --group another-group --generator fernapi/fern-java-sdk --version", - fullRepoPath, - ) - ).stdout, - ); - expect(upgradedJavaVersion).not.toBe(javaVersion); - }); - }, - // 30s timeout - { timeout: 300000 }, + await getBranch(git, JAVA_TEST_BRANCH); + // Make sure we're not including major version bumps in other PRs + const upgradedPythonVersionJavaBranch = cleanStdout( + ( + await execFernCli( + "generator get --group another-group --generator fernapi/fern-python-sdk --version", + fullRepoPath + ) + ).stdout + ); + expect(upgradedPythonVersionJavaBranch).toBe(anotherGroupPythonVersion); + const upgradedJavaVersion = cleanStdout( + ( + await execFernCli( + "generator get --group another-group --generator fernapi/fern-java-sdk --version", + fullRepoPath + ) + ).stdout + ); + expect(upgradedJavaVersion).not.toBe(javaVersion); + }); + }, + // 30s timeout + { timeout: 300000 } ); diff --git a/servers/fern-bot/src/__test__/grpc-proxy.test.ts b/servers/fern-bot/src/__test__/grpc-proxy.test.ts index 27d35a8c0b..c39e1b64fd 100644 --- a/servers/fern-bot/src/__test__/grpc-proxy.test.ts +++ b/servers/fern-bot/src/__test__/grpc-proxy.test.ts @@ -2,159 +2,162 @@ import { expect } from "vitest"; import { proxyGrpc } from "../functions/grpc-proxy/proxyGrpc"; const AWS_BUCKET_NAME = "fdr-api-definition-source-test"; -const AWS_OBJECT_KEY = "fern/fern/2024-08-11T22:35:49.980Z/f6ea473b-1884-4ccc-b386-113cbff139d1"; +const AWS_OBJECT_KEY = + "fern/fern/2024-08-11T22:35:49.980Z/f6ea473b-1884-4ccc-b386-113cbff139d1"; interface ElizaResponse { - sentence: string; + sentence: string; } interface CreateUserRequest { - username: string; - email: string; - age: number; - weight: number; - metadata: object; + username: string; + email: string; + age: number; + weight: number; + metadata: object; } interface UpsertRequest { - namespace: string; - vectors: Vector[]; + namespace: string; + vectors: Vector[]; } interface UpsertResponse { - upsertedCount: number; + upsertedCount: number; } interface Vector { - id: string; - values: number[]; + id: string; + values: number[]; } it.skip("unary w/ gRPC server reflection", async () => { - const response = await proxyGrpc({ - body: { - baseUrl: "https://demo.connectrpc.com", - endpoint: "connectrpc.eliza.v1.ElizaService/Say", - headers: {}, - body: { - sentence: "Feeling happy? Tell me more.", - }, - }, - skipDefaultSchema: true, - }); - - expect(response).not.toBe(null); - - const elizaResponse = response as ElizaResponse; - expect(elizaResponse.sentence).not.toBe(null); + const response = await proxyGrpc({ + body: { + baseUrl: "https://demo.connectrpc.com", + endpoint: "connectrpc.eliza.v1.ElizaService/Say", + headers: {}, + body: { + sentence: "Feeling happy? Tell me more.", + }, + }, + skipDefaultSchema: true, + }); + + expect(response).not.toBe(null); + + const elizaResponse = response as ElizaResponse; + expect(elizaResponse.sentence).not.toBe(null); }); it.skip("unary w/ default schema", async () => { - const response = await proxyGrpc({ - body: { - baseUrl: "https://serverless-test-gb6vrs7.svc.aped-4627-b74a.pinecone.io", - endpoint: "endpoint_index.upsert", - headers: { "Api-Key": process.env.PINECONE_API_KEY }, - body: { - namespace: "test", - vectors: [ - { - id: "v2", - values: [0.1, 0.2, 0.3], - }, - { - id: "v3", - values: [0.4, 0.5, 0.6], - }, - ] as Vector[], - } as UpsertRequest, - }, - }); - - expect(response).not.toBe(null); - - const upsertResponse = JSON.parse(response as string) as UpsertResponse; - expect(upsertResponse.upsertedCount).toBe(2); + const response = await proxyGrpc({ + body: { + baseUrl: "https://serverless-test-gb6vrs7.svc.aped-4627-b74a.pinecone.io", + endpoint: "endpoint_index.upsert", + headers: { "Api-Key": process.env.PINECONE_API_KEY }, + body: { + namespace: "test", + vectors: [ + { + id: "v2", + values: [0.1, 0.2, 0.3], + }, + { + id: "v3", + values: [0.4, 0.5, 0.6], + }, + ] as Vector[], + } as UpsertRequest, + }, + }); + + expect(response).not.toBe(null); + + const upsertResponse = JSON.parse(response as string) as UpsertResponse; + expect(upsertResponse.upsertedCount).toBe(2); }); it.skip("unauthorized", async () => { - const response = await proxyGrpc({ - body: { - baseUrl: "https://serverless-test-gb6vrs7.svc.aped-4627-b74a.pinecone.io", - endpoint: "endpoint_index.upsert", - headers: { Authorization: "Bearer invalid" }, - body: { - namespace: "test", - vectors: [ - { - id: "v2", - values: [0.1, 0.2, 0.3], - }, - { - id: "v3", - values: [0.4, 0.5, 0.6], - }, - ] as Vector[], - } as UpsertRequest, - }, - }); - - expect(response).not.toBe(null); - expect(response).toEqual(`{ + const response = await proxyGrpc({ + body: { + baseUrl: "https://serverless-test-gb6vrs7.svc.aped-4627-b74a.pinecone.io", + endpoint: "endpoint_index.upsert", + headers: { Authorization: "Bearer invalid" }, + body: { + namespace: "test", + vectors: [ + { + id: "v2", + values: [0.1, 0.2, 0.3], + }, + { + id: "v3", + values: [0.4, 0.5, 0.6], + }, + ] as Vector[], + } as UpsertRequest, + }, + }); + + expect(response).not.toBe(null); + expect(response).toEqual(`{ "code": "unauthenticated", "message": "Unauthorized" }`); }); it.skip("invalid schema", async () => { - const response = await proxyGrpc({ - body: { - baseUrl: "https://demo.connectrpc.com", - endpoint: "connectrpc.eliza.v1.ElizaService/Say", - headers: {}, - schema: { - sourceUrl: `https://${AWS_BUCKET_NAME}.s3.amazonaws.com/${AWS_OBJECT_KEY}`, - }, - body: { - sentence: "Feeling happy? Tell me more.", - }, - }, - }); - - expect(response).not.toBe(null); - expect(response).toEqual('Failure: failed to find service named "connectrpc.eliza.v1.ElizaService" in schema'); - - const elizaResponse = response as ElizaResponse; - expect(elizaResponse.sentence).toBe(undefined); + const response = await proxyGrpc({ + body: { + baseUrl: "https://demo.connectrpc.com", + endpoint: "connectrpc.eliza.v1.ElizaService/Say", + headers: {}, + schema: { + sourceUrl: `https://${AWS_BUCKET_NAME}.s3.amazonaws.com/${AWS_OBJECT_KEY}`, + }, + body: { + sentence: "Feeling happy? Tell me more.", + }, + }, + }); + + expect(response).not.toBe(null); + expect(response).toEqual( + 'Failure: failed to find service named "connectrpc.eliza.v1.ElizaService" in schema' + ); + + const elizaResponse = response as ElizaResponse; + expect(elizaResponse.sentence).toBe(undefined); }); it.skip("invalid host", async () => { - const response = await proxyGrpc({ - body: { - baseUrl: "https://demo.connectrpc.com", - endpoint: "user.v1.User/Create", - headers: {}, - schema: { - sourceUrl: `https://${AWS_BUCKET_NAME}.s3.amazonaws.com/${AWS_OBJECT_KEY}`, - }, - body: { - username: "john.doe", - email: "john.doe@gmail.com", - age: 42, - weight: 180.5, - metadata: { - foo: "bar", - }, - } as CreateUserRequest, + const response = await proxyGrpc({ + body: { + baseUrl: "https://demo.connectrpc.com", + endpoint: "user.v1.User/Create", + headers: {}, + schema: { + sourceUrl: `https://${AWS_BUCKET_NAME}.s3.amazonaws.com/${AWS_OBJECT_KEY}`, + }, + body: { + username: "john.doe", + email: "john.doe@gmail.com", + age: 42, + weight: 180.5, + metadata: { + foo: "bar", }, - }); + } as CreateUserRequest, + }, + }); - expect(response).not.toBe(null); - expect(response).toEqual(`{ + expect(response).not.toBe(null); + expect(response).toEqual(`{ "code": "unknown", "message": "HTTP status 302 Found" }`); - const elizaResponse = response as ElizaResponse; - expect(elizaResponse.sentence).toBe(undefined); + const elizaResponse = response as ElizaResponse; + expect(elizaResponse.sentence).toBe(undefined); }); diff --git a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts index 1175246b67..4f7d1f48eb 100644 --- a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts +++ b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts @@ -4,21 +4,24 @@ import { RepoData } from "@libs/schemas"; import { App } from "octokit"; import { updateVersionInternal } from "../shared/updateGeneratorInternal"; -export async function updateGeneratorVersionInternal(env: Env, repoData: RepoData): Promise { - const app: App = setupGithubApp(env); +export async function updateGeneratorVersionInternal( + env: Env, + repoData: RepoData +): Promise { + const app: App = setupGithubApp(env); - // There has to be a better way to do this, but I couldn't find a great way to get the installation ID - await app.eachRepository(async (installation) => { - if (installation.repository.full_name === repoData.full_name) { - await updateVersionInternal( - installation.octokit, - installation.repository, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - env.DEFAULT_FDR_ORIGIN, - env.FERNIE_SLACK_APP_TOKEN, - env.CUSTOMER_ALERTS_SLACK_CHANNEL, - ); - } - }); + // There has to be a better way to do this, but I couldn't find a great way to get the installation ID + await app.eachRepository(async (installation) => { + if (installation.repository.full_name === repoData.full_name) { + await updateVersionInternal( + installation.octokit, + installation.repository, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID, + env.DEFAULT_FDR_ORIGIN, + env.FERNIE_SLACK_APP_TOKEN, + env.CUSTOMER_ALERTS_SLACK_CHANNEL + ); + } + }); } diff --git a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts index 910c838db4..391507649b 100644 --- a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts +++ b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts @@ -4,26 +4,32 @@ import { App } from "octokit"; import { updateVersionInternal } from "../shared/updateGeneratorInternal"; export async function updateGeneratorVersionsInternal(env: Env): Promise { - const app: App = setupGithubApp(env); + const app: App = setupGithubApp(env); - if (env.REPO_TO_RUN_ON !== undefined) { - console.log("REPO_TO_RUN_ON has been specified, only running on:", env.REPO_TO_RUN_ON); + if (env.REPO_TO_RUN_ON !== undefined) { + console.log( + "REPO_TO_RUN_ON has been specified, only running on:", + env.REPO_TO_RUN_ON + ); + } + await app.eachRepository(async (installation) => { + if ( + env.REPO_TO_RUN_ON !== undefined && + installation.repository.full_name !== env.REPO_TO_RUN_ON + ) { + return; + } else if (env.REPO_TO_RUN_ON !== undefined) { + console.log("REPO_TO_RUN_ON has been found, running logic."); } - await app.eachRepository(async (installation) => { - if (env.REPO_TO_RUN_ON !== undefined && installation.repository.full_name !== env.REPO_TO_RUN_ON) { - return; - } else if (env.REPO_TO_RUN_ON !== undefined) { - console.log("REPO_TO_RUN_ON has been found, running logic."); - } - console.log("Encountered installation", installation.repository.full_name); - await updateVersionInternal( - installation.octokit, - installation.repository, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - env.DEFAULT_FDR_ORIGIN, - env.FERNIE_SLACK_APP_TOKEN, - env.CUSTOMER_ALERTS_SLACK_CHANNEL, - ); - }); + console.log("Encountered installation", installation.repository.full_name); + await updateVersionInternal( + installation.octokit, + installation.repository, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID, + env.DEFAULT_FDR_ORIGIN, + env.FERNIE_SLACK_APP_TOKEN, + env.CUSTOMER_ALERTS_SLACK_CHANNEL + ); + }); } diff --git a/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts b/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts index 787a684e0b..07b56dc9dd 100644 --- a/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts +++ b/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts @@ -1,9 +1,22 @@ import { createOrUpdatePullRequest, getOrUpdateBranch } from "@fern-api/github"; import { FernRegistryClient } from "@fern-fern/generators-sdk"; import { ChangelogResponse } from "@fern-fern/generators-sdk/api/resources/generators"; -import { NO_API_FALLBACK_KEY, execFernCli, findFernWorkspaces, getGenerators } from "@libs/fern"; -import { DEFAULT_REMOTE_NAME, cloneRepo, configureGit, type Repository } from "@libs/github/utilities"; -import { GeneratorMessageMetadata, SlackService } from "@libs/slack/SlackService"; +import { + NO_API_FALLBACK_KEY, + execFernCli, + findFernWorkspaces, + getGenerators, +} from "@libs/fern"; +import { + DEFAULT_REMOTE_NAME, + cloneRepo, + configureGit, + type Repository, +} from "@libs/github/utilities"; +import { + GeneratorMessageMetadata, + SlackService, +} from "@libs/slack/SlackService"; import { Octokit } from "octokit"; import SemVer from "semver"; import { CleanOptions, SimpleGit } from "simple-git"; @@ -12,356 +25,421 @@ const PR_BODY_LIMIT = 65000; const MOCK_SERVER_FERN_DIRECTORY = ".mock"; async function getGeneratorChangelog( - fdrUrl: string, - generatorId: string, - from: string, - to: string, + fdrUrl: string, + generatorId: string, + from: string, + to: string ): Promise { - console.log(`Getting changelog for generator ${generatorId} from ${from} to ${to}.`); - const client = new FernRegistryClient({ environment: fdrUrl }); - - const response = await client.generators.versions.getChangelog(generatorId, { - fromVersion: { type: "exclusive", value: from }, - toVersion: { type: "inclusive", value: to }, - }); - if (!response.ok) { - throw new Error(`Changelog for generator ${generatorId} (from version: ${from} to: ${to}) not found`); - } - - return response.body.entries; + console.log( + `Getting changelog for generator ${generatorId} from ${from} to ${to}.` + ); + const client = new FernRegistryClient({ environment: fdrUrl }); + + const response = await client.generators.versions.getChangelog(generatorId, { + fromVersion: { type: "exclusive", value: from }, + toVersion: { type: "inclusive", value: to }, + }); + if (!response.ok) { + throw new Error( + `Changelog for generator ${generatorId} (from version: ${from} to: ${to}) not found` + ); + } + + return response.body.entries; } -async function getCliChangelog(fdrUrl: string, from: string, to: string): Promise { - console.log(`Getting changelog for CLI from ${from} to ${to}`); - const client = new FernRegistryClient({ environment: fdrUrl }); - - const response = await client.generators.cli.getChangelog({ - fromVersion: { type: "exclusive", value: from }, - toVersion: { type: "inclusive", value: to }, - }); - if (!response.ok) { - throw new Error( - `Changelog for CLI (from version: ${from} to: ${to}) not found: ${JSON.stringify(response)} from url ${fdrUrl}`, - ); - } - - console.log("Changelog response: ", JSON.stringify(response.body)); - - return response.body.entries; +async function getCliChangelog( + fdrUrl: string, + from: string, + to: string +): Promise { + console.log(`Getting changelog for CLI from ${from} to ${to}`); + const client = new FernRegistryClient({ environment: fdrUrl }); + + const response = await client.generators.cli.getChangelog({ + fromVersion: { type: "exclusive", value: from }, + toVersion: { type: "inclusive", value: to }, + }); + if (!response.ok) { + throw new Error( + `Changelog for CLI (from version: ${from} to: ${to}) not found: ${JSON.stringify(response)} from url ${fdrUrl}` + ); + } + + console.log("Changelog response: ", JSON.stringify(response.body)); + + return response.body.entries; } function formatChangelogEntry(changelog: ChangelogResponse): string { - let entry = ""; - entry += `\n${changelog.version}\n`; - entry += changelog.changelogEntry - .map((cle) => `
  • \n\n${cle.type}: ${cle.summary}\n
  • `) - .join("\n\n"); - entry += "\n"; - - return entry; + let entry = ""; + entry += `\n${changelog.version}\n`; + entry += changelog.changelogEntry + .map((cle) => `
  • \n\n${cle.type}: ${cle.summary}\n
  • `) + .join("\n\n"); + entry += "\n"; + + return entry; } -function formatChangelogResponses(previousVersion: string, changelogs: ChangelogResponse[]): string { - // The format is effectively the below, where sections are only included if there is at - // least one entry in that section, and we try to cap the number of entries in each section to ~5 with a see more - // ## Upgrading from `` to `` - Changelog - // **`x.y.z`** - // - `fix:` - // **`x.y.z-rc0`** - // - `feat:` - // ... - // > N additional updates, see more - - if (changelogs.length === 0) { - throw new Error("Version difference was found, but no changelog entries were found. This is unexpected."); +function formatChangelogResponses( + previousVersion: string, + changelogs: ChangelogResponse[] +): string { + // The format is effectively the below, where sections are only included if there is at + // least one entry in that section, and we try to cap the number of entries in each section to ~5 with a see more + // ## Upgrading from `` to `` - Changelog + // **`x.y.z`** + // - `fix:` + // **`x.y.z-rc0`** + // - `feat:` + // ... + // > N additional updates, see more + + if (changelogs.length === 0) { + throw new Error( + "Version difference was found, but no changelog entries were found. This is unexpected." + ); + } + + const prBodyTitle = `## Upgrading from \`${previousVersion}\` to \`${changelogs[0]?.version}\` - Changelog\n\n
    \n
    \n
      `; + let prBody = prBodyTitle; + const terminalString = "
    \n
    \n
    "; + + // Get the first 5 changelogs + for (const changelog of changelogs.slice(0, 5)) { + const entry = formatChangelogEntry(changelog); + const terminate = terminateChangelog(prBody, entry, terminalString); + if (terminate != null) { + return terminate; } - - const prBodyTitle = `## Upgrading from \`${previousVersion}\` to \`${changelogs[0]?.version}\` - Changelog\n\n
    \n
    \n
      `; - let prBody = prBodyTitle; - const terminalString = "
    \n
    \n
    "; - - // Get the first 5 changelogs - for (const changelog of changelogs.slice(0, 5)) { - const entry = formatChangelogEntry(changelog); - const terminate = terminateChangelog(prBody, entry, terminalString); - if (terminate != null) { - return terminate; - } - prBody += entry; + prBody += entry; + } + + if (changelogs.length > 5) { + const numChangelogsLeft = changelogs.length - 5; + prBody += `
    \n\t${numChangelogsLeft} additional update${numChangelogsLeft > 1 ? "s" : ""}, see more\n\n
    \n\n`; + + for (const changelog of changelogs.slice(5)) { + let entry = "\t"; + entry += formatChangelogEntry(changelog); + + const terminate = terminateChangelog( + prBody, + entry, + "
    " + terminalString + ); + if (terminate != null) { + return terminate; + } + prBody += entry; } - - if (changelogs.length > 5) { - const numChangelogsLeft = changelogs.length - 5; - prBody += `
    \n\t${numChangelogsLeft} additional update${numChangelogsLeft > 1 ? "s" : ""}, see more\n\n
    \n\n`; - - for (const changelog of changelogs.slice(5)) { - let entry = "\t"; - entry += formatChangelogEntry(changelog); - - const terminate = terminateChangelog(prBody, entry, "
    " + terminalString); - if (terminate != null) { - return terminate; - } - prBody += entry; - } - prBody += ""; - } - return prBody + terminalString; + prBody += ""; + } + return prBody + terminalString; } -function terminateChangelog(prBody: string, newEntry: string, terminalString: string): string | undefined { - if (prBody.length + newEntry.length > PR_BODY_LIMIT) { - return prBody + terminalString; - } - return undefined; +function terminateChangelog( + prBody: string, + newEntry: string, + terminalString: string +): string | undefined { + if (prBody.length + newEntry.length > PR_BODY_LIMIT) { + return prBody + terminalString; + } + return undefined; } // We pollute stdout with a version upgrade log, this tries to ignore that by only consuming the first line // Exported to leverage in tests export function cleanStdout(stdout: string): string { - return stdout.split("╭─")[0]?.split("\n")[0]?.trim() ?? ""; + return stdout.split("╭─")[0]?.split("\n")[0]?.trim() ?? ""; } export async function updateVersionInternal( - octokit: Octokit, - repository: Repository, - fernBotLoginName: string, - fernBotLoginId: string, - fdrUrl: string, - slackToken: string, - slackChannel: string, + octokit: Octokit, + repository: Repository, + fernBotLoginName: string, + fernBotLoginId: string, + fdrUrl: string, + slackToken: string, + slackChannel: string ): Promise { - const [git, fullRepoPath] = await configureGit(repository); - console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); - await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); - - const slackClient = new SlackService(slackToken, slackChannel); - - const client = new FernRegistryClient({ environment: fdrUrl }); - - const fernWorkspaces = await findFernWorkspaces(fullRepoPath); - console.log(`Found ${fernWorkspaces.length} fern workspaces: ${fernWorkspaces.join(", ")}`); - for (const fernWorkspacePath of fernWorkspaces) { - let maybeOrganization: string | undefined; - try { - const result = await execFernCli("organization", fullRepoPath); - maybeOrganization = cleanStdout(typeof result.stdout === "string" ? result.stdout : ""); - console.log(`Found organization ID: ${maybeOrganization}`); - } catch (error) { - console.error( - "Could not determine the repo owner, continuing to upgrade CLI, but will fail generator upgrades.", - error, - ); - } + const [git, fullRepoPath] = await configureGit(repository); + console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); + await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); + + const slackClient = new SlackService(slackToken, slackChannel); + + const client = new FernRegistryClient({ environment: fdrUrl }); + + const fernWorkspaces = await findFernWorkspaces(fullRepoPath); + console.log( + `Found ${fernWorkspaces.length} fern workspaces: ${fernWorkspaces.join(", ")}` + ); + for (const fernWorkspacePath of fernWorkspaces) { + let maybeOrganization: string | undefined; + try { + const result = await execFernCli("organization", fullRepoPath); + maybeOrganization = cleanStdout( + typeof result.stdout === "string" ? result.stdout : "" + ); + console.log(`Found organization ID: ${maybeOrganization}`); + } catch (error) { + console.error( + "Could not determine the repo owner, continuing to upgrade CLI, but will fail generator upgrades.", + error + ); + } - // We skip the mock server as that's not a real Fern workspace - // we'll upgrade the CLI within the proper Fern config repo, so this is skippable - if (fernWorkspacePath.endsWith(MOCK_SERVER_FERN_DIRECTORY)) { - console.log(`Found ${MOCK_SERVER_FERN_DIRECTORY} Fern workspace, skipping.`); - continue; - } + // We skip the mock server as that's not a real Fern workspace + // we'll upgrade the CLI within the proper Fern config repo, so this is skippable + if (fernWorkspacePath.endsWith(MOCK_SERVER_FERN_DIRECTORY)) { + console.log( + `Found ${MOCK_SERVER_FERN_DIRECTORY} Fern workspace, skipping.` + ); + continue; + } - await handleSingleUpgrade({ + await handleSingleUpgrade({ + octokit, + repository, + git, + branchName: "fern/update/cli", + prTitle: "Upgrade Fern CLI", + upgradeAction: async () => { + // Here we have to pipe yes to get through interactive prompts in the CLI + const response = await execFernCli("upgrade", fernWorkspacePath, true); + console.log(response.stdout); + console.log(response.stderr); + }, + getPRBody: async (fromVersion, toVersion) => { + return formatChangelogResponses( + fromVersion, + await getCliChangelog(fdrUrl, fromVersion, toVersion) + ); + }, + getEntityVersion: async () => { + const result = await execFernCli("--version", fernWorkspacePath); + return cleanStdout( + typeof result.stdout === "string" ? result.stdout : "" + ); + }, + slackClient, + maybeOrganization, + }); + + // Pull a branch of fern/update//: + // as well as fern/update/cli + const generatorsList = await getGenerators(fernWorkspacePath); + for (const [apiName, api] of Object.entries(generatorsList)) { + for (const [groupName, group] of Object.entries(api)) { + for (const generator of group) { + const generatorName = cleanStdout(generator); + const branchName = "fern/update/"; + let additionalName = groupName; + if (apiName !== NO_API_FALLBACK_KEY) { + additionalName = `${apiName}/${groupName}`; + } + additionalName = `${generatorName.replace("fernapi/", "")}@${additionalName}`; + + const generatorResponse = await client.generators.getGeneratorByImage( + { dockerImage: generator } + ); + if (!generatorResponse.ok || generatorResponse.body == null) { + throw new Error(`Generator ${generator} not found`); + } + const generatorEntity = generatorResponse.body; + + // We could collect the promises here and await them at the end, but there aren't many you'd parallelize, + // and I think you'd outweigh that benefit by having to make several clones to manage the branches in isolation. + await handleSingleUpgrade({ octokit, repository, git, - branchName: "fern/update/cli", - prTitle: "Upgrade Fern CLI", - upgradeAction: async () => { - // Here we have to pipe yes to get through interactive prompts in the CLI - const response = await execFernCli("upgrade", fernWorkspacePath, true); - console.log(response.stdout); - console.log(response.stderr); + branchName: `${branchName}${additionalName}`, + prTitle: `Upgrade Fern ${generatorEntity.displayName} Generator: (\`${groupName}\`)`, + upgradeAction: async ({ + includeMajor, + }: { + includeMajor?: boolean; + }) => { + let command = `generator upgrade --generator ${generatorName} --group ${groupName}`; + if (apiName !== NO_API_FALLBACK_KEY) { + command += ` --api ${apiName}`; + } + if (includeMajor) { + command += " --include-major"; + } + const response = await execFernCli(command, fernWorkspacePath); + console.log(response.stdout); + console.log(response.stderr); }, getPRBody: async (fromVersion, toVersion) => { - return formatChangelogResponses(fromVersion, await getCliChangelog(fdrUrl, fromVersion, toVersion)); + return formatChangelogResponses( + fromVersion, + await getGeneratorChangelog( + fdrUrl, + generatorEntity.id, + fromVersion, + toVersion + ) + ); }, getEntityVersion: async () => { - const result = await execFernCli("--version", fernWorkspacePath); - return cleanStdout(typeof result.stdout === "string" ? result.stdout : ""); + let command = `generator get --version --generator ${generatorName} --group ${groupName}`; + if (apiName !== NO_API_FALLBACK_KEY) { + command += ` --api ${apiName}`; + } + const result = await execFernCli(command, fernWorkspacePath); + return cleanStdout( + typeof result.stdout === "string" ? result.stdout : "" + ); + }, + maybeGetGeneratorMetadata: async () => { + return { + group: groupName, + generatorName, + apiName: apiName !== NO_API_FALLBACK_KEY ? apiName : undefined, + }; }, slackClient, maybeOrganization, - }); - - // Pull a branch of fern/update//: - // as well as fern/update/cli - const generatorsList = await getGenerators(fernWorkspacePath); - for (const [apiName, api] of Object.entries(generatorsList)) { - for (const [groupName, group] of Object.entries(api)) { - for (const generator of group) { - const generatorName = cleanStdout(generator); - const branchName = "fern/update/"; - let additionalName = groupName; - if (apiName !== NO_API_FALLBACK_KEY) { - additionalName = `${apiName}/${groupName}`; - } - additionalName = `${generatorName.replace("fernapi/", "")}@${additionalName}`; - - const generatorResponse = await client.generators.getGeneratorByImage({ dockerImage: generator }); - if (!generatorResponse.ok || generatorResponse.body == null) { - throw new Error(`Generator ${generator} not found`); - } - const generatorEntity = generatorResponse.body; - - // We could collect the promises here and await them at the end, but there aren't many you'd parallelize, - // and I think you'd outweigh that benefit by having to make several clones to manage the branches in isolation. - await handleSingleUpgrade({ - octokit, - repository, - git, - branchName: `${branchName}${additionalName}`, - prTitle: `Upgrade Fern ${generatorEntity.displayName} Generator: (\`${groupName}\`)`, - upgradeAction: async ({ includeMajor }: { includeMajor?: boolean }) => { - let command = `generator upgrade --generator ${generatorName} --group ${groupName}`; - if (apiName !== NO_API_FALLBACK_KEY) { - command += ` --api ${apiName}`; - } - if (includeMajor) { - command += " --include-major"; - } - const response = await execFernCli(command, fernWorkspacePath); - console.log(response.stdout); - console.log(response.stderr); - }, - getPRBody: async (fromVersion, toVersion) => { - return formatChangelogResponses( - fromVersion, - await getGeneratorChangelog(fdrUrl, generatorEntity.id, fromVersion, toVersion), - ); - }, - getEntityVersion: async () => { - let command = `generator get --version --generator ${generatorName} --group ${groupName}`; - if (apiName !== NO_API_FALLBACK_KEY) { - command += ` --api ${apiName}`; - } - const result = await execFernCli(command, fernWorkspacePath); - return cleanStdout(typeof result.stdout === "string" ? result.stdout : ""); - }, - maybeGetGeneratorMetadata: async () => { - return { - group: groupName, - generatorName, - apiName: apiName !== NO_API_FALLBACK_KEY ? apiName : undefined, - }; - }, - slackClient, - maybeOrganization, - }); - } - } + }); } + } } + } } async function handleSingleUpgrade({ - octokit, - repository, - git, - branchName, - prTitle, - upgradeAction, - getPRBody, - getEntityVersion, - maybeGetGeneratorMetadata, - slackClient, - maybeOrganization, + octokit, + repository, + git, + branchName, + prTitle, + upgradeAction, + getPRBody, + getEntityVersion, + maybeGetGeneratorMetadata, + slackClient, + maybeOrganization, }: { - octokit: Octokit; - repository: Repository; - git: SimpleGit; - branchName: string; - prTitle: string; - upgradeAction: ({ includeMajor }: { includeMajor?: boolean }) => Promise; - getPRBody: (fromVersion: string, toversion: string) => Promise; - getEntityVersion: () => Promise; - maybeGetGeneratorMetadata?: () => Promise; - slackClient: SlackService; - maybeOrganization: string | undefined; + octokit: Octokit; + repository: Repository; + git: SimpleGit; + branchName: string; + prTitle: string; + upgradeAction: ({ + includeMajor, + }: { + includeMajor?: boolean; + }) => Promise; + getPRBody: (fromVersion: string, toversion: string) => Promise; + getEntityVersion: () => Promise; + maybeGetGeneratorMetadata?: () => Promise; + slackClient: SlackService; + maybeOrganization: string | undefined; }): Promise { - // Before we checkout a new branch, we need to ensure we have the current version off the default branch - // Checkout the default branch and run the version command to get the current version - await git.checkout(repository.default_branch); - const fromVersion = await getEntityVersion(); - - // Checkout an upgrade branch, if one exists, update it, otherwise create it - const originDefaultBranch = `${DEFAULT_REMOTE_NAME}/${repository.default_branch}`; - await getOrUpdateBranch(git, originDefaultBranch, branchName); - - // Force reset the branch to the default branch - // This is mostly meant to allow repeating migrations (e.g. we ship a faulty CLI version + migration, if we did not reset the branch - // we'd never rerun the upgrade, now we reset everything and rerun the upgrade, thus rerunning the migration) - await git.fetch([DEFAULT_REMOTE_NAME]); - await git.reset(["--hard", originDefaultBranch]); - await git.clean(CleanOptions.FORCE, ["-d"]); - // Note we don't do this for API spec PRs as those invite users to update their API overrides file to match the API spec changes - // so we'd be blowing away their work if we reset the branch. CLI + generator upgrades should be safe to reset. - - // Perform the upgrade and get the new version you just upgraded to - console.log(`Upgrading entity to latest version, from version: ${fromVersion}`); - await upgradeAction({}); + // Before we checkout a new branch, we need to ensure we have the current version off the default branch + // Checkout the default branch and run the version command to get the current version + await git.checkout(repository.default_branch); + const fromVersion = await getEntityVersion(); + + // Checkout an upgrade branch, if one exists, update it, otherwise create it + const originDefaultBranch = `${DEFAULT_REMOTE_NAME}/${repository.default_branch}`; + await getOrUpdateBranch(git, originDefaultBranch, branchName); + + // Force reset the branch to the default branch + // This is mostly meant to allow repeating migrations (e.g. we ship a faulty CLI version + migration, if we did not reset the branch + // we'd never rerun the upgrade, now we reset everything and rerun the upgrade, thus rerunning the migration) + await git.fetch([DEFAULT_REMOTE_NAME]); + await git.reset(["--hard", originDefaultBranch]); + await git.clean(CleanOptions.FORCE, ["-d"]); + // Note we don't do this for API spec PRs as those invite users to update their API overrides file to match the API spec changes + // so we'd be blowing away their work if we reset the branch. CLI + generator upgrades should be safe to reset. + + // Perform the upgrade and get the new version you just upgraded to + console.log( + `Upgrading entity to latest version, from version: ${fromVersion}` + ); + await upgradeAction({}); + const toVersion = await getEntityVersion(); + console.log(`Upgraded entity to latest version, to version: ${toVersion}`); + + console.log("Checking for changes to commit and push"); + if (!(await git.status()).isClean() && fromVersion !== toVersion) { + console.log("Changes detected, committing and pushing"); + // Add + commit files + await git.add(["-A"]); + // TODO: use AI to generate commit messages from the changelog + await git.commit("(chore): upgrade versions to latest"); + + // Push the changes + await git.push([ + "--force-with-lease", + DEFAULT_REMOTE_NAME, + `${branchName}:refs/heads/${branchName}`, + ]); + + // Open a PR, or update it in place + const prUrl = await createOrUpdatePullRequest( + octokit, + { + title: `:herb: :sparkles: [Scheduled] ${prTitle}`, + base: "main", + body: await getPRBody(fromVersion, toVersion), + }, + repository.full_name, + repository.full_name, + branchName + ); + + // Notify via slack that the upgrade PR was created + await slackClient.notifyUpgradePRCreated({ + fromVersion, + toVersion, + prUrl, + repoName: repository.full_name, + generator: maybeGetGeneratorMetadata + ? await maybeGetGeneratorMetadata() + : undefined, + maybeOrganization, + }); + return; + } else if (fromVersion === toVersion) { + console.log( + "Versions were the same, let's see if there's a new version across major versions." + ); + await upgradeAction({ includeMajor: true }); const toVersion = await getEntityVersion(); - console.log(`Upgraded entity to latest version, to version: ${toVersion}`); - - console.log("Checking for changes to commit and push"); - if (!(await git.status()).isClean() && fromVersion !== toVersion) { - console.log("Changes detected, committing and pushing"); - // Add + commit files - await git.add(["-A"]); - // TODO: use AI to generate commit messages from the changelog - await git.commit("(chore): upgrade versions to latest"); - - // Push the changes - await git.push(["--force-with-lease", DEFAULT_REMOTE_NAME, `${branchName}:refs/heads/${branchName}`]); - - // Open a PR, or update it in place - const prUrl = await createOrUpdatePullRequest( - octokit, - { - title: `:herb: :sparkles: [Scheduled] ${prTitle}`, - base: "main", - body: await getPRBody(fromVersion, toVersion), - }, - repository.full_name, - repository.full_name, - branchName, - ); - - // Notify via slack that the upgrade PR was created - await slackClient.notifyUpgradePRCreated({ - fromVersion, - toVersion, - prUrl, - repoName: repository.full_name, - generator: maybeGetGeneratorMetadata ? await maybeGetGeneratorMetadata() : undefined, - maybeOrganization, - }); - return; - } else if (fromVersion === toVersion) { - console.log("Versions were the same, let's see if there's a new version across major versions."); - await upgradeAction({ includeMajor: true }); - const toVersion = await getEntityVersion(); - const parsedFrom = SemVer.parse(fromVersion); - const parsedTo = SemVer.parse(toVersion); - // Clean the branch back up, to remove any unstaged changes - await git.reset(["--hard"]); - - if (parsedFrom == null || parsedTo == null) { - console.log("An invalid version was found, quitting", fromVersion, toVersion); - return; - } - if (parsedFrom.major < parsedTo.major) { - slackClient.notifyMajorVersionUpgradeEncountered({ - repoUrl: repository.html_url, - repoName: repository.full_name, - currentVersion: fromVersion, - maybeOrganization, - generator: maybeGetGeneratorMetadata ? await maybeGetGeneratorMetadata() : undefined, - }); - console.log("No change made as the upgrade is across major versions."); - return; - } + const parsedFrom = SemVer.parse(fromVersion); + const parsedTo = SemVer.parse(toVersion); + // Clean the branch back up, to remove any unstaged changes + await git.reset(["--hard"]); + + if (parsedFrom == null || parsedTo == null) { + console.log( + "An invalid version was found, quitting", + fromVersion, + toVersion + ); + return; + } + if (parsedFrom.major < parsedTo.major) { + slackClient.notifyMajorVersionUpgradeEncountered({ + repoUrl: repository.html_url, + repoName: repository.full_name, + currentVersion: fromVersion, + maybeOrganization, + generator: maybeGetGeneratorMetadata + ? await maybeGetGeneratorMetadata() + : undefined, + }); + console.log("No change made as the upgrade is across major versions."); + return; } + } - console.log("No changes detected, skipping PR creation"); + console.log("No changes detected, skipping PR creation"); } diff --git a/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersion.ts b/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersion.ts index 7cba8d037e..76ed4776a2 100644 --- a/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersion.ts +++ b/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersion.ts @@ -4,20 +4,25 @@ import { RepoData } from "@libs/schemas"; import { updateGeneratorVersionInternal } from "./actions/updateGeneratorVersion"; const updateGeneratorVersion = async (event: unknown) => { - console.debug("Beginning scheduled run of `updateGeneratorVersion`, received event:", event); + console.debug( + "Beginning scheduled run of `updateGeneratorVersion`, received event:", + event + ); - // Only run on Mondays - const today = new Date(); - if (today.getDay() === 1) { - // 1 is Monday - console.debug("It's Monday! Running weekly task."); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - return updateGeneratorVersionInternal(env, event as RepoData); - } else { - console.debug("It's not Monday, skipping weekly task."); - return; - } + // Only run on Mondays + const today = new Date(); + if (today.getDay() === 1) { + // 1 is Monday + console.debug("It's Monday! Running weekly task."); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + return updateGeneratorVersionInternal(env, event as RepoData); + } else { + console.debug("It's not Monday, skipping weekly task."); + return; + } }; export const handler = handlerWrapper(updateGeneratorVersion); diff --git a/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersions.ts b/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersions.ts index 8c54f60a79..83e0f0f64d 100644 --- a/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersions.ts +++ b/servers/fern-bot/src/functions/generator-updates/updateGeneratorVersions.ts @@ -2,11 +2,18 @@ import { evaluateEnv } from "@libs/env"; import { handlerWrapper } from "@libs/handler-wrapper"; import { updateGeneratorVersionsInternal } from "./actions/updateGeneratorVersions"; -export const updateGeneratorVersions = async (_event: unknown): Promise => { - console.debug("Beginning scheduled run of `updateGeneratorVersions`, received event:", _event); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - await updateGeneratorVersionsInternal(env); +export const updateGeneratorVersions = async ( + _event: unknown +): Promise => { + console.debug( + "Beginning scheduled run of `updateGeneratorVersions`, received event:", + _event + ); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + await updateGeneratorVersionsInternal(env); }; export const handler = handlerWrapper(updateGeneratorVersions); diff --git a/servers/fern-bot/src/functions/grpc-proxy/actions/constants.ts b/servers/fern-bot/src/functions/grpc-proxy/actions/constants.ts index 4e53330e00..a907cd4ec7 100644 --- a/servers/fern-bot/src/functions/grpc-proxy/actions/constants.ts +++ b/servers/fern-bot/src/functions/grpc-proxy/actions/constants.ts @@ -1,3 +1,3 @@ export const DEFAULT_PROTO_DIRECTORY = "proto"; export const DEFAULT_PROTO_SOURCE_URL = - "https://fdr-dev2-api-definition-source-files.s3.us-east-1.amazonaws.com/pinecone/api/2024-09-30T21%3A29%3A51.859Z/10095813-78e1-4999-9a9f-6f08d1084609?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNE6LAYO7B%2F20240930%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240930T212951Z&X-Amz-Expires=604800&X-Amz-Signature=67f5548b27f26ea33a5f9bec23885d6159c8d6d5deb01f99744ce34898b791f4&X-Amz-SignedHeaders=host&x-id=GetObject"; + "https://fdr-dev2-api-definition-source-files.s3.us-east-1.amazonaws.com/pinecone/api/2024-09-30T21%3A29%3A51.859Z/10095813-78e1-4999-9a9f-6f08d1084609?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNE6LAYO7B%2F20240930%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240930T212951Z&X-Amz-Expires=604800&X-Amz-Signature=67f5548b27f26ea33a5f9bec23885d6159c8d6d5deb01f99744ce34898b791f4&X-Amz-SignedHeaders=host&x-id=GetObject"; diff --git a/servers/fern-bot/src/functions/grpc-proxy/actions/proxyGrpc.ts b/servers/fern-bot/src/functions/grpc-proxy/actions/proxyGrpc.ts index 613b5b0408..3889356547 100644 --- a/servers/fern-bot/src/functions/grpc-proxy/actions/proxyGrpc.ts +++ b/servers/fern-bot/src/functions/grpc-proxy/actions/proxyGrpc.ts @@ -1,4 +1,8 @@ -import { GrpcProxyRequest, GrpcProxyResponse, ProtobufSchema } from "@generated/api"; +import { + GrpcProxyRequest, + GrpcProxyResponse, + ProtobufSchema, +} from "@generated/api"; import { Buf } from "@libs/buf"; import { fetchAndUnzip } from "@utils/fetchAndUnzip"; import { mkdir } from "fs/promises"; @@ -7,92 +11,98 @@ import tmp from "tmp-promise"; import { DEFAULT_PROTO_DIRECTORY, DEFAULT_PROTO_SOURCE_URL } from "./constants"; interface Options { - skipDefaultSchema?: boolean; + skipDefaultSchema?: boolean; } export async function proxyGrpcInternal({ - request, - options, + request, + options, }: { - request: GrpcProxyRequest; - options?: Options; + request: GrpcProxyRequest; + options?: Options; }): Promise { - const buf = new Buf(); - try { - const response = await buf.curl({ - request: await toCurlRequest({ request, options }), - }); - return { - body: response.body, - }; - } catch (error) { - console.debug("Failed to proxy gRPC request:", error); - if (error instanceof Error) { - return { - body: error.message, - }; - } - throw error; + const buf = new Buf(); + try { + const response = await buf.curl({ + request: await toCurlRequest({ request, options }), + }); + return { + body: response.body, + }; + } catch (error) { + console.debug("Failed to proxy gRPC request:", error); + if (error instanceof Error) { + return { + body: error.message, + }; } + throw error; + } } async function toCurlRequest({ - request, - options, + request, + options, }: { - request: GrpcProxyRequest; - options?: Options; + request: GrpcProxyRequest; + options?: Options; }): Promise { - return { - baseUrl: request.baseUrl, - endpoint: getGrpcPathForEndpoint(request.endpoint), - headers: getCurlHeaders(request.headers), - schema: - request.schema != null - ? await fetchProtobufSchema(request.schema) - : !options?.skipDefaultSchema - ? await fetchProtobufSchema({ type: "remote", sourceUrl: DEFAULT_PROTO_SOURCE_URL }) - : undefined, - grpc: true, - body: request.body, - }; + return { + baseUrl: request.baseUrl, + endpoint: getGrpcPathForEndpoint(request.endpoint), + headers: getCurlHeaders(request.headers), + schema: + request.schema != null + ? await fetchProtobufSchema(request.schema) + : !options?.skipDefaultSchema + ? await fetchProtobufSchema({ + type: "remote", + sourceUrl: DEFAULT_PROTO_SOURCE_URL, + }) + : undefined, + grpc: true, + body: request.body, + }; } async function fetchProtobufSchema(schema: ProtobufSchema): Promise { - const absolutePathToProtoDirectory = path.join((await tmp.dir()).path, DEFAULT_PROTO_DIRECTORY); - console.debug(`mkdir ${absolutePathToProtoDirectory}`); - await mkdir(absolutePathToProtoDirectory, { recursive: true }); + const absolutePathToProtoDirectory = path.join( + (await tmp.dir()).path, + DEFAULT_PROTO_DIRECTORY + ); + console.debug(`mkdir ${absolutePathToProtoDirectory}`); + await mkdir(absolutePathToProtoDirectory, { recursive: true }); - await fetchAndUnzip({ - destination: absolutePathToProtoDirectory, - sourceUrl: schema.sourceUrl, - }); + await fetchAndUnzip({ + destination: absolutePathToProtoDirectory, + sourceUrl: schema.sourceUrl, + }); - return absolutePathToProtoDirectory; + return absolutePathToProtoDirectory; } function getGrpcPathForEndpoint(endpoint: string): string { - // TODO: Temporary mapping between endpoint ID and gRPC path. - switch (endpoint) { - case "endpoint_index.delete": - return "VectorService/Delete"; - case "endpoint_index.describe_index_stats": - return "VectorService/DescribeIndexStats"; - case "endpoint_index.fetch": - return "VectorService/Fetch"; - case "endpoint_index.list": - return "VectorService/List"; - case "endpoint_index.update": - return "VectorService/Update"; - case "endpoint_index.upsert": - return "VectorService/Upsert"; - case "endpoint_index.query": - return "VectorService/Query"; - default: - return endpoint; - } + // TODO: Temporary mapping between endpoint ID and gRPC path. + switch (endpoint) { + case "endpoint_index.delete": + return "VectorService/Delete"; + case "endpoint_index.describe_index_stats": + return "VectorService/DescribeIndexStats"; + case "endpoint_index.fetch": + return "VectorService/Fetch"; + case "endpoint_index.list": + return "VectorService/List"; + case "endpoint_index.update": + return "VectorService/Update"; + case "endpoint_index.upsert": + return "VectorService/Upsert"; + case "endpoint_index.query": + return "VectorService/Query"; + default: + return endpoint; + } } function getCurlHeaders(headers: Record): string[] { - return Object.entries(headers).map(([key, value]) => `${key}: ${value}`); + return Object.entries(headers).map(([key, value]) => `${key}: ${value}`); } diff --git a/servers/fern-bot/src/functions/grpc-proxy/proxyGrpc.ts b/servers/fern-bot/src/functions/grpc-proxy/proxyGrpc.ts index ff406640c9..b2d47f6b71 100644 --- a/servers/fern-bot/src/functions/grpc-proxy/proxyGrpc.ts +++ b/servers/fern-bot/src/functions/grpc-proxy/proxyGrpc.ts @@ -5,23 +5,31 @@ import { proxyGrpcInternal } from "./actions/proxyGrpc"; // The relevant lambda request properties, referenced from // https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads interface LambdaHttpRequestPayload { - body: string | null; + body: string | null; - // This is a custom property that we've added to the payload. - skipDefaultSchema?: boolean; + // This is a custom property that we've added to the payload. + skipDefaultSchema?: boolean; } -export const proxyGrpc = async (payload: LambdaHttpRequestPayload): Promise => { - if (payload.body == null) { - throw new Error("No body received"); - } - console.debug("Proxying gRPC request, received payload:", JSON.stringify(payload)); - const response = await proxyGrpcInternal({ - request: JSON.parse(payload.body) as GrpcProxyRequest, - options: payload.skipDefaultSchema != null ? { skipDefaultSchema: payload.skipDefaultSchema } : undefined, - }); - console.debug("Received gRPC response body:", response.body); - return response.body; +export const proxyGrpc = async ( + payload: LambdaHttpRequestPayload +): Promise => { + if (payload.body == null) { + throw new Error("No body received"); + } + console.debug( + "Proxying gRPC request, received payload:", + JSON.stringify(payload) + ); + const response = await proxyGrpcInternal({ + request: JSON.parse(payload.body) as GrpcProxyRequest, + options: + payload.skipDefaultSchema != null + ? { skipDefaultSchema: payload.skipDefaultSchema } + : undefined, + }); + console.debug("Received gRPC response body:", response.body); + return response.body; }; export const handler = handlerWrapper(proxyGrpc); diff --git a/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpec.ts b/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpec.ts index ff4d393f11..04264e998a 100644 --- a/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpec.ts +++ b/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpec.ts @@ -4,18 +4,21 @@ import { RepoData } from "@libs/schemas"; import { App } from "octokit"; import { updateSpecInternal } from "../shared/updateSpecInternal"; -export async function updateOpenApiSpecInternal(env: Env, repoData: RepoData): Promise { - const app: App = setupGithubApp(env); +export async function updateOpenApiSpecInternal( + env: Env, + repoData: RepoData +): Promise { + const app: App = setupGithubApp(env); - // There has to be a better way to do this, but I couldn't find a great way to get the installation ID - await app.eachRepository(async (installation) => { - if (installation.repository.full_name === repoData.full_name) { - await updateSpecInternal( - installation.octokit, - installation.repository, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - ); - } - }); + // There has to be a better way to do this, but I couldn't find a great way to get the installation ID + await app.eachRepository(async (installation) => { + if (installation.repository.full_name === repoData.full_name) { + await updateSpecInternal( + installation.octokit, + installation.repository, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID + ); + } + }); } diff --git a/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpecs.ts b/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpecs.ts index d17fe68194..69c19a8da2 100644 --- a/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpecs.ts +++ b/servers/fern-bot/src/functions/oas-cron/actions/updateOpenApiSpecs.ts @@ -4,33 +4,39 @@ import { App } from "octokit"; import { updateSpecInternal } from "../shared/updateSpecInternal"; export async function updateOpenApiSpecsInternal(env: Env): Promise { - const app: App = setupGithubApp(env); + const app: App = setupGithubApp(env); - let foundRepo = false; - if (env.REPO_TO_RUN_ON !== undefined) { - console.log("REPO_TO_RUN_ON has been specified, only running on:", env.REPO_TO_RUN_ON); + let foundRepo = false; + if (env.REPO_TO_RUN_ON !== undefined) { + console.log( + "REPO_TO_RUN_ON has been specified, only running on:", + env.REPO_TO_RUN_ON + ); + } + await app.eachRepository(async (installation) => { + // Github repo and org names are case insentitive, so we should compare them as both lowercase + if ( + env.REPO_TO_RUN_ON !== undefined && + installation.repository.full_name.toLowerCase() !== + env.REPO_TO_RUN_ON.toLowerCase() + ) { + return; + } else if (env.REPO_TO_RUN_ON !== undefined) { + console.log("REPO_TO_RUN_ON has been found, running logic."); + foundRepo = true; } - await app.eachRepository(async (installation) => { - // Github repo and org names are case insentitive, so we should compare them as both lowercase - if ( - env.REPO_TO_RUN_ON !== undefined && - installation.repository.full_name.toLowerCase() !== env.REPO_TO_RUN_ON.toLowerCase() - ) { - return; - } else if (env.REPO_TO_RUN_ON !== undefined) { - console.log("REPO_TO_RUN_ON has been found, running logic."); - foundRepo = true; - } - await updateSpecInternal( - installation.octokit, - installation.repository, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - ); - }); + await updateSpecInternal( + installation.octokit, + installation.repository, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID + ); + }); - if (!foundRepo && env.REPO_TO_RUN_ON !== undefined) { - console.log("REPO_TO_RUN_ON has been specified, but no matching repos were found, so no action was taken."); - } + if (!foundRepo && env.REPO_TO_RUN_ON !== undefined) { + console.log( + "REPO_TO_RUN_ON has been specified, but no matching repos were found, so no action was taken." + ); + } } diff --git a/servers/fern-bot/src/functions/oas-cron/shared/updateSpecInternal.ts b/servers/fern-bot/src/functions/oas-cron/shared/updateSpecInternal.ts index 05b639e04e..e370417da9 100644 --- a/servers/fern-bot/src/functions/oas-cron/shared/updateSpecInternal.ts +++ b/servers/fern-bot/src/functions/oas-cron/shared/updateSpecInternal.ts @@ -1,62 +1,72 @@ import { createOrUpdatePullRequest, getOrUpdateBranch } from "@fern-api/github"; import { generateChangelog, generateCommitMessage } from "@libs/cohere"; import { execFernCli, findFernWorkspaces } from "@libs/fern"; -import { DEFAULT_REMOTE_NAME, Repository, cloneRepo, configureGit } from "@libs/github"; +import { + DEFAULT_REMOTE_NAME, + Repository, + cloneRepo, + configureGit, +} from "@libs/github"; import { Octokit } from "octokit"; const OPENAPI_UPDATE_BRANCH = "fern/update-api-specs"; export async function updateSpecInternal( - octokit: Octokit, - repository: Repository, - fernBotLoginName: string, - fernBotLoginId: string, + octokit: Octokit, + repository: Repository, + fernBotLoginName: string, + fernBotLoginId: string ): Promise { - const [git, fullRepoPath] = await configureGit(repository); - const originDefaultBranch = `${DEFAULT_REMOTE_NAME}/${repository.default_branch}`; + const [git, fullRepoPath] = await configureGit(repository); + const originDefaultBranch = `${DEFAULT_REMOTE_NAME}/${repository.default_branch}`; - console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); - await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); - await getOrUpdateBranch(git, originDefaultBranch, OPENAPI_UPDATE_BRANCH); + console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); + await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); + await getOrUpdateBranch(git, originDefaultBranch, OPENAPI_UPDATE_BRANCH); - const fernWorkspaces = await findFernWorkspaces(fullRepoPath); - for (const fernWorkspacePath of fernWorkspaces) { - // Run API update command which will pull the new spec from the specified - // origin and write it to disk we can then commit it to github from there. - await execFernCli("api update", fernWorkspacePath); - console.log("Checking for changes to commit and push"); - if (!(await git.status()).isClean()) { - console.log("Changes detected, committing and pushing"); - // Add + commit files - const commitDiff = await git.diff(); - await git.add(["-A"]); - await git.commit(await generateCommitMessage(commitDiff, "chore: update API specification")); + const fernWorkspaces = await findFernWorkspaces(fullRepoPath); + for (const fernWorkspacePath of fernWorkspaces) { + // Run API update command which will pull the new spec from the specified + // origin and write it to disk we can then commit it to github from there. + await execFernCli("api update", fernWorkspacePath); + console.log("Checking for changes to commit and push"); + if (!(await git.status()).isClean()) { + console.log("Changes detected, committing and pushing"); + // Add + commit files + const commitDiff = await git.diff(); + await git.add(["-A"]); + await git.commit( + await generateCommitMessage( + commitDiff, + "chore: update API specification" + ) + ); - // Push the changes - await git.push([ - "--force-with-lease", - DEFAULT_REMOTE_NAME, - `${OPENAPI_UPDATE_BRANCH}:refs/heads/${OPENAPI_UPDATE_BRANCH}`, - ]); + // Push the changes + await git.push([ + "--force-with-lease", + DEFAULT_REMOTE_NAME, + `${OPENAPI_UPDATE_BRANCH}:refs/heads/${OPENAPI_UPDATE_BRANCH}`, + ]); - const fullDiff = await git.diff([originDefaultBranch]); - // Open a PR - await createOrUpdatePullRequest( - octokit, - { - title: ":herb: :sparkles: [Scheduled] Update API Spec", - base: "main", - body: await generateChangelog( - fullDiff, - "This PR updates your API Definition to the latest version.", - ), - }, - repository.full_name, - repository.full_name, - OPENAPI_UPDATE_BRANCH, - ); - } else { - console.log("No changes detected, skipping PR creation"); - } + const fullDiff = await git.diff([originDefaultBranch]); + // Open a PR + await createOrUpdatePullRequest( + octokit, + { + title: ":herb: :sparkles: [Scheduled] Update API Spec", + base: "main", + body: await generateChangelog( + fullDiff, + "This PR updates your API Definition to the latest version." + ), + }, + repository.full_name, + repository.full_name, + OPENAPI_UPDATE_BRANCH + ); + } else { + console.log("No changes detected, skipping PR creation"); } + } } diff --git a/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpec.ts b/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpec.ts index d520f14d24..28d04c64ce 100644 --- a/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpec.ts +++ b/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpec.ts @@ -4,10 +4,15 @@ import { RepoData } from "@libs/schemas"; import { updateOpenApiSpecInternal } from "./actions/updateOpenApiSpec"; const updateOpenApiSpec = async (event: unknown) => { - console.debug("Beginning scheduled run of `updateOpenApiSpecs`, received event:", event); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - return updateOpenApiSpecInternal(env, event as RepoData); + console.debug( + "Beginning scheduled run of `updateOpenApiSpecs`, received event:", + event + ); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + return updateOpenApiSpecInternal(env, event as RepoData); }; export const handler = handlerWrapper(updateOpenApiSpec); diff --git a/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpecs.ts b/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpecs.ts index e9d266477d..475e38b0fb 100644 --- a/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpecs.ts +++ b/servers/fern-bot/src/functions/oas-cron/updateOpenApiSpecs.ts @@ -3,10 +3,15 @@ import { handlerWrapper } from "@libs/handler-wrapper"; import { updateOpenApiSpecsInternal } from "./actions/updateOpenApiSpecs"; const updateOpenApiSpec = async (_event: unknown) => { - console.debug("Beginning scheduled run of `updateOpenApiSpec`, received event:", _event); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - return updateOpenApiSpecsInternal(env); + console.debug( + "Beginning scheduled run of `updateOpenApiSpec`, received event:", + _event + ); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + return updateOpenApiSpecsInternal(env); }; export const handler = handlerWrapper(updateOpenApiSpec); diff --git a/servers/fern-bot/src/functions/stale-notifs/actions/sendStaleNotifications.ts b/servers/fern-bot/src/functions/stale-notifs/actions/sendStaleNotifications.ts index 9d5d0d1ae8..d518d4d596 100644 --- a/servers/fern-bot/src/functions/stale-notifs/actions/sendStaleNotifications.ts +++ b/servers/fern-bot/src/functions/stale-notifs/actions/sendStaleNotifications.ts @@ -1,5 +1,8 @@ import { FernRegistryClient } from "@fern-fern/paged-generators-sdk"; -import { PullRequest, PullRequestState } from "@fern-fern/paged-generators-sdk/api"; +import { + PullRequest, + PullRequestState, +} from "@fern-fern/paged-generators-sdk/api"; import { Env } from "@libs/env"; import { SlackService } from "@libs/slack/SlackService"; @@ -7,95 +10,117 @@ const STALE_IN_DAYS = 7; const STALE_IN_MS = STALE_IN_DAYS * 24 * 60 * 60 * 1000; const VERBOSE_MESSAGE_THRESHOLD = 5; const FERN_TEAM = new Set([ - "abvthecity", - "amckinney", - "armandobelardo", - "rohinbhargava", - "dsinghvi", - "dcb6", - "chdeskur", - "dannysheridan", - "fern-bot", + "abvthecity", + "amckinney", + "armandobelardo", + "rohinbhargava", + "dsinghvi", + "dcb6", + "chdeskur", + "dannysheridan", + "fern-bot", ]); // We don't care about notifying for our own orgs const EXCLUDE_ORGS = new Set(["fern-api", "fern-demo"]); export async function sendStaleNotificationsInternal(env: Env): Promise { - const client = new FernRegistryClient({ environment: env.DEFAULT_FDR_ORIGIN, token: env.FERN_TOKEN }); - const botPulls = await client.git.listPullRequests({ - // Is the author any fern member, or the github app? - author: [env.GITHUB_APP_LOGIN_NAME], - state: [PullRequestState.Open], - }); + const client = new FernRegistryClient({ + environment: env.DEFAULT_FDR_ORIGIN, + token: env.FERN_TOKEN, + }); + const botPulls = await client.git.listPullRequests({ + // Is the author any fern member, or the github app? + author: [env.GITHUB_APP_LOGIN_NAME], + state: [PullRequestState.Open], + }); - const orgPullMap = new Map(); - let staleBotPRsFound = false; - for await (const pull of botPulls) { - if (env.ENVIRONMENT !== "development" && EXCLUDE_ORGS.has(pull.repositoryOwner)) { - continue; - } + const orgPullMap = new Map(); + let staleBotPRsFound = false; + for await (const pull of botPulls) { + if ( + env.ENVIRONMENT !== "development" && + EXCLUDE_ORGS.has(pull.repositoryOwner) + ) { + continue; + } - if (pull.createdAt < new Date(Date.now() - STALE_IN_MS)) { - orgPullMap.set(pull.repositoryOwner, [...(orgPullMap.get(pull.repositoryOwner) || []), pull]); - staleBotPRsFound = true; - } + if (pull.createdAt < new Date(Date.now() - STALE_IN_MS)) { + orgPullMap.set(pull.repositoryOwner, [ + ...(orgPullMap.get(pull.repositoryOwner) || []), + pull, + ]); + staleBotPRsFound = true; } + } - // Notify stale upgrade PRs to CUSTOMER_ALERTS_SLACK_CHANNEL - const upgradesSlackClient = new SlackService(env.FERNIE_SLACK_APP_TOKEN, env.CUSTOMER_ALERTS_SLACK_CHANNEL); - for (const [org, pulls] of orgPullMap) { - let maybeApiSpecPull: PullRequest | undefined; - const versionUpdatePulls: PullRequest[] = []; - for (const pull of pulls) { - if (pull.title.includes("Update API Spec")) { - maybeApiSpecPull = pull; - } else { - versionUpdatePulls.push(pull); - } - } - if (maybeApiSpecPull != null || versionUpdatePulls.length > 0) { - const allPulls = versionUpdatePulls.concat(maybeApiSpecPull ? [maybeApiSpecPull] : []); - const aPull = allPulls[0]; - upgradesSlackClient.notifyStaleUpgradePRs({ - organization: org, - apiSpecPull: maybeApiSpecPull, - versionUpdatePulls, - // We only call this function if there's at least one PR so this should be safe - repoName: `${aPull?.repositoryOwner}/${aPull?.repositoryName}`, - retoolLink: "https://buildwithfern.retool.com/apps/703271ca-7777-11ef-aecd-ab097775918e/Stale%20Pulls", - // Truncate the messages if we're going to be writing a lot of them - shouldBeVerbose: versionUpdatePulls.length <= VERBOSE_MESSAGE_THRESHOLD, - }); - } + // Notify stale upgrade PRs to CUSTOMER_ALERTS_SLACK_CHANNEL + const upgradesSlackClient = new SlackService( + env.FERNIE_SLACK_APP_TOKEN, + env.CUSTOMER_ALERTS_SLACK_CHANNEL + ); + for (const [org, pulls] of orgPullMap) { + let maybeApiSpecPull: PullRequest | undefined; + const versionUpdatePulls: PullRequest[] = []; + for (const pull of pulls) { + if (pull.title.includes("Update API Spec")) { + maybeApiSpecPull = pull; + } else { + versionUpdatePulls.push(pull); + } } - if (!staleBotPRsFound) { - console.log("No stale fern-bot PRs found"); + if (maybeApiSpecPull != null || versionUpdatePulls.length > 0) { + const allPulls = versionUpdatePulls.concat( + maybeApiSpecPull ? [maybeApiSpecPull] : [] + ); + const aPull = allPulls[0]; + upgradesSlackClient.notifyStaleUpgradePRs({ + organization: org, + apiSpecPull: maybeApiSpecPull, + versionUpdatePulls, + // We only call this function if there's at least one PR so this should be safe + repoName: `${aPull?.repositoryOwner}/${aPull?.repositoryName}`, + retoolLink: + "https://buildwithfern.retool.com/apps/703271ca-7777-11ef-aecd-ab097775918e/Stale%20Pulls", + // Truncate the messages if we're going to be writing a lot of them + shouldBeVerbose: versionUpdatePulls.length <= VERBOSE_MESSAGE_THRESHOLD, + }); } + } + if (!staleBotPRsFound) { + console.log("No stale fern-bot PRs found"); + } - const teamPulls = await client.git.listPullRequests({ - // Is the author any fern member, or the github app? - author: Array.from(FERN_TEAM.values()), - state: [PullRequestState.Open], - }); - const teamPullsSlackClient = new SlackService(env.FERNIE_SLACK_APP_TOKEN, env.CUSTOMER_PULLS_SLACK_CHANNEL); - // Notify on any PRs opened by us to CUSTOMER_PULLS_SLACK_CHANNEL - let numStaleTeamPulls = 0; - for await (const pull of teamPulls) { - if (env.ENVIRONMENT !== "development" && EXCLUDE_ORGS.has(pull.repositoryOwner)) { - continue; - } - if (pull.createdAt < new Date(Date.now() - STALE_IN_MS)) { - numStaleTeamPulls++; - } + const teamPulls = await client.git.listPullRequests({ + // Is the author any fern member, or the github app? + author: Array.from(FERN_TEAM.values()), + state: [PullRequestState.Open], + }); + const teamPullsSlackClient = new SlackService( + env.FERNIE_SLACK_APP_TOKEN, + env.CUSTOMER_PULLS_SLACK_CHANNEL + ); + // Notify on any PRs opened by us to CUSTOMER_PULLS_SLACK_CHANNEL + let numStaleTeamPulls = 0; + for await (const pull of teamPulls) { + if ( + env.ENVIRONMENT !== "development" && + EXCLUDE_ORGS.has(pull.repositoryOwner) + ) { + continue; } - if (numStaleTeamPulls > 0) { - teamPullsSlackClient.notifyStaleTeamPulls({ - numStaleTeamPulls, - retoolLink: "https://buildwithfern.retool.com/apps/703271ca-7777-11ef-aecd-ab097775918e/Stale%20Pulls", - }); - } else { - console.log("No stale team PRs found"); + if (pull.createdAt < new Date(Date.now() - STALE_IN_MS)) { + numStaleTeamPulls++; } + } + if (numStaleTeamPulls > 0) { + teamPullsSlackClient.notifyStaleTeamPulls({ + numStaleTeamPulls, + retoolLink: + "https://buildwithfern.retool.com/apps/703271ca-7777-11ef-aecd-ab097775918e/Stale%20Pulls", + }); + } else { + console.log("No stale team PRs found"); + } } // The below seems to not be possible with installation auth/a github app, so we'd need to use a personal access token diff --git a/servers/fern-bot/src/functions/stale-notifs/sendStaleNotifications.ts b/servers/fern-bot/src/functions/stale-notifs/sendStaleNotifications.ts index 8fafa7528b..71fbe9378f 100644 --- a/servers/fern-bot/src/functions/stale-notifs/sendStaleNotifications.ts +++ b/servers/fern-bot/src/functions/stale-notifs/sendStaleNotifications.ts @@ -3,10 +3,15 @@ import { handlerWrapper } from "@libs/handler-wrapper"; import { sendStaleNotificationsInternal } from "./actions/sendStaleNotifications"; const sendStaleNotifications = async (event: unknown) => { - console.debug("Beginning scheduled run of `sendStaleNotifications`, received event:", event); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - return await sendStaleNotificationsInternal(env); + console.debug( + "Beginning scheduled run of `sendStaleNotifications`, received event:", + event + ); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + return await sendStaleNotificationsInternal(env); }; export const handler = handlerWrapper(sendStaleNotifications); diff --git a/servers/fern-bot/src/functions/update-fdr-repo-data/actions/updateFDRRepoData.ts b/servers/fern-bot/src/functions/update-fdr-repo-data/actions/updateFDRRepoData.ts index 32b9a8b44b..48dfd86ed9 100644 --- a/servers/fern-bot/src/functions/update-fdr-repo-data/actions/updateFDRRepoData.ts +++ b/servers/fern-bot/src/functions/update-fdr-repo-data/actions/updateFDRRepoData.ts @@ -1,7 +1,16 @@ import { FernRegistryClient } from "@fern-fern/generators-sdk"; -import { PullRequestReviewer, PullRequestState } from "@fern-fern/generators-sdk/api"; +import { + PullRequestReviewer, + PullRequestState, +} from "@fern-fern/generators-sdk/api"; import { Env } from "@libs/env"; -import { NO_API_FALLBACK_KEY, cleanFernStdout, execFernCli, findFernWorkspaces, getGenerators } from "@libs/fern"; +import { + NO_API_FALLBACK_KEY, + cleanFernStdout, + execFernCli, + findFernWorkspaces, + getGenerators, +} from "@libs/fern"; import { PullRequest, Repository, setupGithubApp } from "@libs/github"; import { cloneRepo, configureGit } from "@libs/github/utilities"; import { RepoData } from "@libs/schemas"; @@ -11,202 +20,226 @@ import tmp from "tmp-promise"; // Note given we're making requests to FDR, this could take time, so we're parallelizing this function with a Map step in // the step function, as we do for all the other actions. -export async function updateFDRRepoDataInternal(env: Env, repoData: RepoData | undefined): Promise { - const app: App = setupGithubApp(env); +export async function updateFDRRepoDataInternal( + env: Env, + repoData: RepoData | undefined +): Promise { + const app: App = setupGithubApp(env); - // Get repo data for the given repo - await app.eachRepository(async (installation) => { - if (repoData && installation.repository.full_name !== repoData.full_name) { - return; - } - await updateRepoDb( - app, - installation.repository, - installation.octokit, - env.GITHUB_APP_LOGIN_NAME, - env.GITHUB_APP_LOGIN_ID, - env.DEFAULT_FDR_ORIGIN, - env.FERN_TOKEN, - ); - }); + // Get repo data for the given repo + await app.eachRepository(async (installation) => { + if (repoData && installation.repository.full_name !== repoData.full_name) { + return; + } + await updateRepoDb( + app, + installation.repository, + installation.octokit, + env.GITHUB_APP_LOGIN_NAME, + env.GITHUB_APP_LOGIN_ID, + env.DEFAULT_FDR_ORIGIN, + env.FERN_TOKEN + ); + }); } async function updateRepoDb( - app: App, - repository: Repository, - octokit: Octokit, - fernBotLoginName: string, - fernBotLoginId: string, - fdrUrl: string, - fernToken: string, + app: App, + repository: Repository, + octokit: Octokit, + fernBotLoginName: string, + fernBotLoginId: string, + fdrUrl: string, + fernToken: string ): Promise { - console.log(`Updating repo data at ${fdrUrl} with token ${fernToken}`); - const client = new FernRegistryClient({ environment: fdrUrl, token: fernToken }); - - const [git, fullRepoPath] = await configureGit(repository); - console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); - await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); - - const fernWorkspaces = await findFernWorkspaces(fullRepoPath); - for (const fernWorkspacePath of fernWorkspaces) { - try { - // Try this to see if it's a fern config repo, there are probably better ways to do this - const generatorsList = await getGenerators(fernWorkspacePath); - const result = await execFernCli("organization", fernWorkspacePath); - const organizationId = cleanFernStdout(typeof result.stdout === "string" ? result.stdout : ""); - - // Update config repo in FDR - const upsertResponse = await client.git.upsertRepository({ - type: "config", - id: { - type: "github", - id: repository.id.toString(), - }, - name: repository.name, - owner: repository.owner.login, - fullName: repository.full_name, - url: repository.html_url, - repositoryOwnerOrganizationId: organizationId, - // TODO(FER-2517): actually track and action checks - defaultBranchChecks: [], - }); - - if (!upsertResponse.ok) { - console.log( - `Failed to upsert configuration repo, bailing out: ${JSON.stringify(upsertResponse.error)}`, - ); - return; + console.log(`Updating repo data at ${fdrUrl} with token ${fernToken}`); + const client = new FernRegistryClient({ + environment: fdrUrl, + token: fernToken, + }); + + const [git, fullRepoPath] = await configureGit(repository); + console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); + await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); + + const fernWorkspaces = await findFernWorkspaces(fullRepoPath); + for (const fernWorkspacePath of fernWorkspaces) { + try { + // Try this to see if it's a fern config repo, there are probably better ways to do this + const generatorsList = await getGenerators(fernWorkspacePath); + const result = await execFernCli("organization", fernWorkspacePath); + const organizationId = cleanFernStdout( + typeof result.stdout === "string" ? result.stdout : "" + ); + + // Update config repo in FDR + const upsertResponse = await client.git.upsertRepository({ + type: "config", + id: { + type: "github", + id: repository.id.toString(), + }, + name: repository.name, + owner: repository.owner.login, + fullName: repository.full_name, + url: repository.html_url, + repositoryOwnerOrganizationId: organizationId, + // TODO(FER-2517): actually track and action checks + defaultBranchChecks: [], + }); + + if (!upsertResponse.ok) { + console.log( + `Failed to upsert configuration repo, bailing out: ${JSON.stringify(upsertResponse.error)}` + ); + return; + } + await getAndUpsertPulls(client, octokit, repository); + + for (const [apiName, api] of Object.entries(generatorsList)) { + for (const [groupName, group] of Object.entries(api)) { + for (const generator of group) { + const tmpDir = await tmp.dir(); + const repoJsonFileName = `${tmpDir.path}/${repository.id.toString()}.json`; + let command = `generator get --repository --language --generator ${generator} --group ${groupName} -o ${repoJsonFileName}`; + if (apiName !== NO_API_FALLBACK_KEY) { + command += ` --api ${apiName}`; } - await getAndUpsertPulls(client, octokit, repository); - - for (const [apiName, api] of Object.entries(generatorsList)) { - for (const [groupName, group] of Object.entries(api)) { - for (const generator of group) { - const tmpDir = await tmp.dir(); - const repoJsonFileName = `${tmpDir.path}/${repository.id.toString()}.json`; - let command = `generator get --repository --language --generator ${generator} --group ${groupName} -o ${repoJsonFileName}`; - if (apiName !== NO_API_FALLBACK_KEY) { - command += ` --api ${apiName}`; - } - - await execFernCli(command, fernWorkspacePath); - const maybeRepo = await readFile(repoJsonFileName, "utf8"); - if (maybeRepo?.length > 0) { - // Of the form { repository: string, language: string } - const generatorsYmlRepo = JSON.parse(maybeRepo); - if ("repository" in generatorsYmlRepo && generatorsYmlRepo.repository.length > 0) { - const octokitRepo = await getRepository(app, generatorsYmlRepo.repository); - if (octokitRepo) { - await client.git.upsertRepository({ - type: "sdk", - sdkLanguage: generatorsYmlRepo.language, - id: { - type: "github", - id: octokitRepo.id.toString(), - }, - name: octokitRepo.name, - owner: octokitRepo.owner.login, - fullName: octokitRepo.full_name, - url: octokitRepo.url, - repositoryOwnerOrganizationId: organizationId, - // TODO(FER-2517): actually track and action checks - defaultBranchChecks: [], - }); - - await getAndUpsertPulls(client, octokit, octokitRepo); - } - } - } - } + + await execFernCli(command, fernWorkspacePath); + const maybeRepo = await readFile(repoJsonFileName, "utf8"); + if (maybeRepo?.length > 0) { + // Of the form { repository: string, language: string } + const generatorsYmlRepo = JSON.parse(maybeRepo); + if ( + "repository" in generatorsYmlRepo && + generatorsYmlRepo.repository.length > 0 + ) { + const octokitRepo = await getRepository( + app, + generatorsYmlRepo.repository + ); + if (octokitRepo) { + await client.git.upsertRepository({ + type: "sdk", + sdkLanguage: generatorsYmlRepo.language, + id: { + type: "github", + id: octokitRepo.id.toString(), + }, + name: octokitRepo.name, + owner: octokitRepo.owner.login, + fullName: octokitRepo.full_name, + url: octokitRepo.url, + repositoryOwnerOrganizationId: organizationId, + // TODO(FER-2517): actually track and action checks + defaultBranchChecks: [], + }); + + await getAndUpsertPulls(client, octokit, octokitRepo); } + } } - } catch (e) { - console.log( - `Found a repo that was not a Fern config repo, or not a high enough version, skipping...: ${(e as Error).message}`, - ); + } } + } + } catch (e) { + console.log( + `Found a repo that was not a Fern config repo, or not a high enough version, skipping...: ${(e as Error).message}` + ); } + } } -async function getAndUpsertPulls(client: FernRegistryClient, octokit: Octokit, repository: Repository) { - // Get all PRs on repo, update PRs in FDR - const pulls = await octokit.paginate(octokit.rest.pulls.list, { - state: "all", - owner: repository.owner.login, - repo: repository.name, - }); - - for (const pull of pulls) { - try { - await client.git.upsertPullRequest({ - pullRequestNumber: pull.number, - repositoryName: repository.name, - repositoryOwner: repository.owner.login, - author: pull.user - ? { - username: pull.user.login, - // These can be null or undefined, but we're just taking them as the same and making them undefined - email: pull.user.email ?? undefined, - name: pull.user.name ?? undefined, - } - : undefined, - reviewers: getReviewers(pull as PullRequest), - title: pull.title, - url: pull.html_url, - // TODO(FER-2517): actually track and action checks - checks: [], - // This should be a safe cast - state: pull.state as PullRequestState, - createdAt: new Date(pull.created_at), - updatedAt: new Date(pull.updated_at), - mergedAt: pull.merged_at != null ? new Date(pull.merged_at) : undefined, - closedAt: pull.closed_at != null ? new Date(pull.closed_at) : undefined, - }); - } catch (e) { - console.error( - `Error updating PR ${pull.number} on repo ${repository.full_name}: ${e}, likely we have not registered this repo, quitting.`, - ); - return; - } +async function getAndUpsertPulls( + client: FernRegistryClient, + octokit: Octokit, + repository: Repository +) { + // Get all PRs on repo, update PRs in FDR + const pulls = await octokit.paginate(octokit.rest.pulls.list, { + state: "all", + owner: repository.owner.login, + repo: repository.name, + }); + + for (const pull of pulls) { + try { + await client.git.upsertPullRequest({ + pullRequestNumber: pull.number, + repositoryName: repository.name, + repositoryOwner: repository.owner.login, + author: pull.user + ? { + username: pull.user.login, + // These can be null or undefined, but we're just taking them as the same and making them undefined + email: pull.user.email ?? undefined, + name: pull.user.name ?? undefined, + } + : undefined, + reviewers: getReviewers(pull as PullRequest), + title: pull.title, + url: pull.html_url, + // TODO(FER-2517): actually track and action checks + checks: [], + // This should be a safe cast + state: pull.state as PullRequestState, + createdAt: new Date(pull.created_at), + updatedAt: new Date(pull.updated_at), + mergedAt: pull.merged_at != null ? new Date(pull.merged_at) : undefined, + closedAt: pull.closed_at != null ? new Date(pull.closed_at) : undefined, + }); + } catch (e) { + console.error( + `Error updating PR ${pull.number} on repo ${repository.full_name}: ${e}, likely we have not registered this repo, quitting.` + ); + return; } + } } function getReviewers(pull: PullRequest): PullRequestReviewer[] { - const reviewers: PullRequestReviewer[] = []; - if (pull.requested_reviewers != null) { - for (const reviewer of pull.requested_reviewers) { - reviewers.push({ - type: "user", - username: reviewer.login, - // These can be null or undefined, but we're just taking them as the same and making them undefined - email: reviewer.email ?? undefined, - name: reviewer.name ?? undefined, - }); - } + const reviewers: PullRequestReviewer[] = []; + if (pull.requested_reviewers != null) { + for (const reviewer of pull.requested_reviewers) { + reviewers.push({ + type: "user", + username: reviewer.login, + // These can be null or undefined, but we're just taking them as the same and making them undefined + email: reviewer.email ?? undefined, + name: reviewer.name ?? undefined, + }); } - - if (pull.requested_teams != null) { - for (const team_reviewer of pull.requested_teams) { - reviewers.push({ - type: "team", - name: team_reviewer.name, - teamId: team_reviewer.id.toString(), - }); - } + } + + if (pull.requested_teams != null) { + for (const team_reviewer of pull.requested_teams) { + reviewers.push({ + type: "team", + name: team_reviewer.name, + teamId: team_reviewer.id.toString(), + }); } + } - return reviewers; + return reviewers; } // Does octokit not expose a better way to do this??? -async function getRepository(app: App, repositoryFullName: string): Promise { - let maybeRepo: Repository | undefined = undefined; - await app.eachRepository((installation) => { - // repo and organization names are case insensitive, so the full name is as well - if (installation.repository.full_name.toLowerCase() === repositoryFullName.toLowerCase()) { - maybeRepo = installation.repository; - } - }); +async function getRepository( + app: App, + repositoryFullName: string +): Promise { + let maybeRepo: Repository | undefined = undefined; + await app.eachRepository((installation) => { + // repo and organization names are case insensitive, so the full name is as well + if ( + installation.repository.full_name.toLowerCase() === + repositoryFullName.toLowerCase() + ) { + maybeRepo = installation.repository; + } + }); - return maybeRepo; + return maybeRepo; } diff --git a/servers/fern-bot/src/functions/update-fdr-repo-data/updateFDRRepoData.ts b/servers/fern-bot/src/functions/update-fdr-repo-data/updateFDRRepoData.ts index 4c6b0295f2..7fa8172590 100644 --- a/servers/fern-bot/src/functions/update-fdr-repo-data/updateFDRRepoData.ts +++ b/servers/fern-bot/src/functions/update-fdr-repo-data/updateFDRRepoData.ts @@ -4,10 +4,15 @@ import { updateFDRRepoDataInternal } from "./actions/updateFDRRepoData"; import { RepoData } from "@libs/schemas"; const updateFDRRepoData = async (event: unknown) => { - console.debug("Beginning scheduled run of `updateFDRRepoData`, received event:", event); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - return await updateFDRRepoDataInternal(env, event as RepoData | undefined); + console.debug( + "Beginning scheduled run of `updateFDRRepoData`, received event:", + event + ); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + return await updateFDRRepoDataInternal(env, event as RepoData | undefined); }; export const handler = handlerWrapper(updateFDRRepoData); diff --git a/servers/fern-bot/src/functions/update-repo-data/actions/updateRepoData.ts b/servers/fern-bot/src/functions/update-repo-data/actions/updateRepoData.ts index 4493858023..4663d3798b 100644 --- a/servers/fern-bot/src/functions/update-repo-data/actions/updateRepoData.ts +++ b/servers/fern-bot/src/functions/update-repo-data/actions/updateRepoData.ts @@ -5,39 +5,39 @@ import { json2csv } from "json-2-csv"; import { App } from "octokit"; interface RepoData { - id: string; - name: string; - full_name: string; - default_branch: string; - clone_url: string; + id: string; + name: string; + full_name: string; + default_branch: string; + clone_url: string; } // In order to parallelize actioning on the data (e.g. updating openapi specs) // we first write the data to s3 and then trigger the action from there. export async function updateRepoDataInternal(env: Env): Promise { - const app: App = setupGithubApp(env); - const repos: RepoData[] = []; + const app: App = setupGithubApp(env); + const repos: RepoData[] = []; - // Get repo data - await app.eachRepository((installation) => { - repos.push({ - id: installation.repository.id.toString(), - name: installation.repository.name, - full_name: installation.repository.full_name, - default_branch: installation.repository.default_branch, - clone_url: installation.repository.clone_url, - }); + // Get repo data + await app.eachRepository((installation) => { + repos.push({ + id: installation.repository.id.toString(), + name: installation.repository.name, + full_name: installation.repository.full_name, + default_branch: installation.repository.default_branch, + clone_url: installation.repository.clone_url, }); + }); - // Write the data to S3 - const csvContent = json2csv(repos); - const bucket = env.REPO_DATA_S3_BUCKET ?? "fern-bot-data"; - const key = env.REPO_DATA_S3_KEY ?? "lambdas/repos.csv"; - console.log(`Writing repo data to S3 at ${bucket}/${key}`); - const s3 = new S3(); - await s3.putObject({ - Bucket: bucket, - Key: key, - Body: csvContent, - }); + // Write the data to S3 + const csvContent = json2csv(repos); + const bucket = env.REPO_DATA_S3_BUCKET ?? "fern-bot-data"; + const key = env.REPO_DATA_S3_KEY ?? "lambdas/repos.csv"; + console.log(`Writing repo data to S3 at ${bucket}/${key}`); + const s3 = new S3(); + await s3.putObject({ + Bucket: bucket, + Key: key, + Body: csvContent, + }); } diff --git a/servers/fern-bot/src/functions/update-repo-data/updateRepoData.ts b/servers/fern-bot/src/functions/update-repo-data/updateRepoData.ts index 9ad9e20ecb..084825f968 100644 --- a/servers/fern-bot/src/functions/update-repo-data/updateRepoData.ts +++ b/servers/fern-bot/src/functions/update-repo-data/updateRepoData.ts @@ -3,10 +3,15 @@ import { handlerWrapper } from "@libs/handler-wrapper"; import { updateRepoDataInternal } from "./actions/updateRepoData"; const updateRepoData = async (event: unknown) => { - console.debug("Beginning scheduled run of `updateRepoData`, received event:", event); - const env = evaluateEnv(); - console.debug("Environment evaluated, continuing to actual action execution."); - return updateRepoDataInternal(env); + console.debug( + "Beginning scheduled run of `updateRepoData`, received event:", + event + ); + const env = evaluateEnv(); + console.debug( + "Environment evaluated, continuing to actual action execution." + ); + return updateRepoDataInternal(env); }; export const handler = handlerWrapper(updateRepoData); diff --git a/servers/fern-bot/src/libs/buf.ts b/servers/fern-bot/src/libs/buf.ts index d2838c8a9a..607a075116 100644 --- a/servers/fern-bot/src/libs/buf.ts +++ b/servers/fern-bot/src/libs/buf.ts @@ -7,114 +7,125 @@ const BUF_NPM_PACKAGE = "@bufbuild/buf"; const BUF_VERSION = "1.42.0"; export declare namespace Buf { - export interface CurlRequest { - /** The base URL to use for the call (e.g. https://acme.co) */ - baseUrl: string; - /** The gRPC endpoint name (e.g. user.v1.UserService/GetUser) */ - endpoint: string; - /** The headers to send along with the request (e.g. 'Authorization: Bearer ...') */ - headers: string[]; - /** - * The path to the Protobuf schema files that define this API. If not specified, - * it's assumed the server supports gRPC reflection. - */ - schema?: string; - /** Use gRPC to send the request. */ - grpc?: boolean; - /** The request body (represented as JSON) to include in the request, if any */ - body?: unknown; - } + export interface CurlRequest { + /** The base URL to use for the call (e.g. https://acme.co) */ + baseUrl: string; + /** The gRPC endpoint name (e.g. user.v1.UserService/GetUser) */ + endpoint: string; + /** The headers to send along with the request (e.g. 'Authorization: Bearer ...') */ + headers: string[]; + /** + * The path to the Protobuf schema files that define this API. If not specified, + * it's assumed the server supports gRPC reflection. + */ + schema?: string; + /** Use gRPC to send the request. */ + grpc?: boolean; + /** The request body (represented as JSON) to include in the request, if any */ + body?: unknown; + } - export interface CurlResponse { - /** The response body received from the call, if any */ - body?: unknown; - } + export interface CurlResponse { + /** The response body received from the call, if any */ + body?: unknown; + } } export type CLI = (args?: string[]) => ReturnType; export class Buf { - private cli: CLI | undefined; + private cli: CLI | undefined; - public async curl({ request }: { request: Buf.CurlRequest }): Promise { - const cli = await this.getOrInstall(); - const response = await cli(this.getArgsForCurlRequest(request)); - if (response.exitCode !== 0) { - return { - body: response.stderr, - }; - } - return { - body: response.stdout, - }; + public async curl({ + request, + }: { + request: Buf.CurlRequest; + }): Promise { + const cli = await this.getOrInstall(); + const response = await cli(this.getArgsForCurlRequest(request)); + if (response.exitCode !== 0) { + return { + body: response.stderr, + }; } + return { + body: response.stdout, + }; + } - private async getOrInstall(): Promise { - if (this.cli) { - return this.cli; - } - const which = createLoggingExecutable("which", { - cwd: process.cwd(), - }); - try { - await which(["buf"]); - } catch (err) { - console.log("buf is not installed", err); - return this.install(); - } - this.cli = this.createBufExecutable(); - return this.cli; + private async getOrInstall(): Promise { + if (this.cli) { + return this.cli; } + const which = createLoggingExecutable("which", { + cwd: process.cwd(), + }); + try { + await which(["buf"]); + } catch (err) { + console.log("buf is not installed", err); + return this.install(); + } + this.cli = this.createBufExecutable(); + return this.cli; + } - private async install(): Promise { - // Running the commands on Lambdas is a bit odd...specifically you can only write to tmp on a lambda - // so here we make sure the CLI is bundled via the `external` block in serverless.yml - // and then execute the command directly via node_modules, with the home and cache set to /tmp. - const tmpDir = await tmp.dir(); - const tmpDirPath = tmpDir.path; - process.env.NPM_CONFIG_CACHE = `${tmpDirPath}/.npm`; - process.env.HOME = tmpDirPath; - - // Update config to allow `npm install` to work from within the `fern upgrade` command - process.env.NPM_CONFIG_PREFIX = tmpDirPath; - // Re-install the CLI to ensure it's at the correct path, given the updated config - const install = await execa("npm", ["install", "-g", `${BUF_NPM_PACKAGE}@${BUF_VERSION}`]); - if (install.exitCode === 0) { - console.log(`Successfully installed ${BUF_NPM_PACKAGE}`); - } else { - const message = `Failed to install buf \n${install.stdout}\n${install.stderr}`; - console.log(message); - throw new Error(message); - } + private async install(): Promise { + // Running the commands on Lambdas is a bit odd...specifically you can only write to tmp on a lambda + // so here we make sure the CLI is bundled via the `external` block in serverless.yml + // and then execute the command directly via node_modules, with the home and cache set to /tmp. + const tmpDir = await tmp.dir(); + const tmpDirPath = tmpDir.path; + process.env.NPM_CONFIG_CACHE = `${tmpDirPath}/.npm`; + process.env.HOME = tmpDirPath; - this.cli = this.createBufExecutable(); - return this.cli; + // Update config to allow `npm install` to work from within the `fern upgrade` command + process.env.NPM_CONFIG_PREFIX = tmpDirPath; + // Re-install the CLI to ensure it's at the correct path, given the updated config + const install = await execa("npm", [ + "install", + "-g", + `${BUF_NPM_PACKAGE}@${BUF_VERSION}`, + ]); + if (install.exitCode === 0) { + console.log(`Successfully installed ${BUF_NPM_PACKAGE}`); + } else { + const message = `Failed to install buf \n${install.stdout}\n${install.stderr}`; + console.log(message); + throw new Error(message); } - private getArgsForCurlRequest(request: Buf.CurlRequest): string[] { - const args = ["curl", this.getFullyQualifiedEndpoint(request)]; - for (const header of request.headers) { - args.push(...["--header", header]); - } - if (request.schema != null) { - args.push(...["--schema", request.schema]); - } - if (request.grpc) { - args.push(...["--protocol", "grpc"]); - } - if (request.body != null) { - args.push(...["--data", JSON.stringify(request.body)]); - } - return args; - } + this.cli = this.createBufExecutable(); + return this.cli; + } - private getFullyQualifiedEndpoint({ baseUrl, endpoint }: Buf.CurlRequest): string { - return urlJoin(baseUrl, endpoint); + private getArgsForCurlRequest(request: Buf.CurlRequest): string[] { + const args = ["curl", this.getFullyQualifiedEndpoint(request)]; + for (const header of request.headers) { + args.push(...["--header", header]); } - - private createBufExecutable(): CLI { - return (args) => { - return execa("npx", ["buf", ...(args ?? [])]); - }; + if (request.schema != null) { + args.push(...["--schema", request.schema]); + } + if (request.grpc) { + args.push(...["--protocol", "grpc"]); + } + if (request.body != null) { + args.push(...["--data", JSON.stringify(request.body)]); } + return args; + } + + private getFullyQualifiedEndpoint({ + baseUrl, + endpoint, + }: Buf.CurlRequest): string { + return urlJoin(baseUrl, endpoint); + } + + private createBufExecutable(): CLI { + return (args) => { + return execa("npx", ["buf", ...(args ?? [])]); + }; + } } diff --git a/servers/fern-bot/src/libs/cohere.ts b/servers/fern-bot/src/libs/cohere.ts index 11b3ae9c56..5c0b1c5d77 100644 --- a/servers/fern-bot/src/libs/cohere.ts +++ b/servers/fern-bot/src/libs/cohere.ts @@ -3,45 +3,51 @@ import { CohereClient } from "cohere-ai"; const DEFAULT_GITHUB_MESSAGE = "[Scheduled] Update API Spec"; async function coChat(prompt: string): Promise { - const co = new CohereClient(); - const response = await co.chat({ model: "command-r-plus", message: prompt }); + const co = new CohereClient(); + const response = await co.chat({ model: "command-r-plus", message: prompt }); - if (response.finishReason !== "COMPLETE") { - return DEFAULT_GITHUB_MESSAGE; - } + if (response.finishReason !== "COMPLETE") { + return DEFAULT_GITHUB_MESSAGE; + } - return response.text; + return response.text; } -export async function generateCommitMessage(diff: string, fallbackMessage: string): Promise { - if (diff === "") { - return DEFAULT_GITHUB_MESSAGE; - } +export async function generateCommitMessage( + diff: string, + fallbackMessage: string +): Promise { + if (diff === "") { + return DEFAULT_GITHUB_MESSAGE; + } - const prompt = `Given the following git diff, write a short and professional but descriptive commit message that strictly follows the Conventional Commits specification validated via regex r'^(feat|fix|docs|style|refactor|test|chore)(([w-]+))?: .+$. + const prompt = `Given the following git diff, write a short and professional but descriptive commit message that strictly follows the Conventional Commits specification validated via regex r'^(feat|fix|docs|style|refactor|test|chore)(([w-]+))?: .+$. The commit message should be a summary of all the changes within the diff and should provide as much detail as possible to give context to the changes, while remaining short and concise. This is important, don't hallucinate this. \`\`\` ${diff} \`\`\` `; - try { - return await coChat(prompt); - } catch (error) { - console.error( - `Call to Cohere failed generating commit message, with error: ${(error as Error).message}, using fallback message: ${fallbackMessage}`, - ); + try { + return await coChat(prompt); + } catch (error) { + console.error( + `Call to Cohere failed generating commit message, with error: ${(error as Error).message}, using fallback message: ${fallbackMessage}` + ); - return fallbackMessage; - } + return fallbackMessage; + } } -export async function generateChangelog(diff: string, fallbackMessage: string): Promise { - if (diff === "") { - return DEFAULT_GITHUB_MESSAGE; - } +export async function generateChangelog( + diff: string, + fallbackMessage: string +): Promise { + if (diff === "") { + return DEFAULT_GITHUB_MESSAGE; + } - const prompt = `You are an OpenAPI expert, your goal is to help me write a changelog for the following OpenAPI spec diff. The changelog should be concise, informative and user friendly. This is important, don't hallucinate this. + const prompt = `You are an OpenAPI expert, your goal is to help me write a changelog for the following OpenAPI spec diff. The changelog should be concise, informative and user friendly. This is important, don't hallucinate this. Here is an example of what a changelog should look like: diff: \`\`\` @@ -223,13 +229,13 @@ export async function generateChangelog(diff: string, fallbackMessage: string): \`\`\` `; - try { - return await coChat(prompt); - } catch (error) { - console.error( - `Call to Cohere failed writing the PR body, with error: ${(error as Error).message}, using fallback message: ${fallbackMessage}`, - ); + try { + return await coChat(prompt); + } catch (error) { + console.error( + `Call to Cohere failed writing the PR body, with error: ${(error as Error).message}, using fallback message: ${fallbackMessage}` + ); - return fallbackMessage; - } + return fallbackMessage; + } } diff --git a/servers/fern-bot/src/libs/env.ts b/servers/fern-bot/src/libs/env.ts index dd46ee2ead..4b9b8fecf4 100644 --- a/servers/fern-bot/src/libs/env.ts +++ b/servers/fern-bot/src/libs/env.ts @@ -3,48 +3,58 @@ export const OMIT = "OMIT"; export interface Env { - GITHUB_APP_LOGIN_NAME: string; - GITHUB_APP_LOGIN_ID: string; - GITHUB_APP_ID: string; - GITHUB_APP_PRIVATE_KEY: string; - GITHUB_APP_CLIENT_ID: string; - GITHUB_APP_CLIENT_SECRET: string; - GITHUB_APP_WEBHOOK_SECRET: string; - REPO_TO_RUN_ON?: string; - REPO_DATA_S3_BUCKET?: string; - REPO_DATA_S3_KEY?: string; - DEFAULT_VENUS_ORIGIN: string; - DEFAULT_FDR_ORIGIN: string; - CUSTOMER_ALERTS_SLACK_CHANNEL: string; - CUSTOMER_PULLS_SLACK_CHANNEL: string; - FERNIE_SLACK_APP_TOKEN: string; - FERN_TOKEN: string; - ENVIRONMENT: string; + GITHUB_APP_LOGIN_NAME: string; + GITHUB_APP_LOGIN_ID: string; + GITHUB_APP_ID: string; + GITHUB_APP_PRIVATE_KEY: string; + GITHUB_APP_CLIENT_ID: string; + GITHUB_APP_CLIENT_SECRET: string; + GITHUB_APP_WEBHOOK_SECRET: string; + REPO_TO_RUN_ON?: string; + REPO_DATA_S3_BUCKET?: string; + REPO_DATA_S3_KEY?: string; + DEFAULT_VENUS_ORIGIN: string; + DEFAULT_FDR_ORIGIN: string; + CUSTOMER_ALERTS_SLACK_CHANNEL: string; + CUSTOMER_PULLS_SLACK_CHANNEL: string; + FERNIE_SLACK_APP_TOKEN: string; + FERN_TOKEN: string; + ENVIRONMENT: string; } export function evaluateEnv(): Env { - const repoToRunOn = process?.env.REPO_TO_RUN_ON; - const repoDataS3Bucket = process?.env.REPO_DATA_S3_BUCKET; - const repoDataS3Key = process?.env.REPO_DATA_S3_KEY; + const repoToRunOn = process?.env.REPO_TO_RUN_ON; + const repoDataS3Bucket = process?.env.REPO_DATA_S3_BUCKET; + const repoDataS3Key = process?.env.REPO_DATA_S3_KEY; - // These assertions are technically unsafe, but we don't want the bot to deploy without them - return { - GITHUB_APP_LOGIN_NAME: process?.env.GITHUB_APP_LOGIN_NAME!, - GITHUB_APP_LOGIN_ID: process?.env.GITHUB_APP_LOGIN_ID!, - GITHUB_APP_ID: process?.env.GITHUB_APP_ID!, - GITHUB_APP_PRIVATE_KEY: process?.env.GITHUB_APP_PRIVATE_KEY?.replaceAll("\\n", "\n")!, - GITHUB_APP_CLIENT_ID: process?.env.GITHUB_APP_CLIENT_ID!, - GITHUB_APP_CLIENT_SECRET: process?.env.GITHUB_APP_CLIENT_SECRET!, - GITHUB_APP_WEBHOOK_SECRET: process?.env.GITHUB_APP_WEBHOOK_SECRET!, - REPO_TO_RUN_ON: repoToRunOn == null || repoToRunOn === OMIT ? undefined : repoToRunOn, - REPO_DATA_S3_BUCKET: repoDataS3Bucket == null || repoDataS3Bucket === OMIT ? undefined : repoDataS3Bucket, - REPO_DATA_S3_KEY: repoDataS3Key == null || repoDataS3Key === OMIT ? undefined : repoDataS3Key, - DEFAULT_VENUS_ORIGIN: process?.env.DEFAULT_VENUS_ORIGIN!, - DEFAULT_FDR_ORIGIN: process?.env.DEFAULT_FDR_ORIGIN!, - FERNIE_SLACK_APP_TOKEN: process?.env.FERNIE_SLACK_APP_TOKEN!, - CUSTOMER_ALERTS_SLACK_CHANNEL: process?.env.CUSTOMER_ALERTS_SLACK_CHANNEL!, - CUSTOMER_PULLS_SLACK_CHANNEL: process?.env.CUSTOMER_PULLS_SLACK_CHANNEL!, - FERN_TOKEN: process?.env.FERN_TOKEN!, - ENVIRONMENT: process?.env.ENVIRONMENT!, - }; + // These assertions are technically unsafe, but we don't want the bot to deploy without them + return { + GITHUB_APP_LOGIN_NAME: process?.env.GITHUB_APP_LOGIN_NAME!, + GITHUB_APP_LOGIN_ID: process?.env.GITHUB_APP_LOGIN_ID!, + GITHUB_APP_ID: process?.env.GITHUB_APP_ID!, + GITHUB_APP_PRIVATE_KEY: process?.env.GITHUB_APP_PRIVATE_KEY?.replaceAll( + "\\n", + "\n" + )!, + GITHUB_APP_CLIENT_ID: process?.env.GITHUB_APP_CLIENT_ID!, + GITHUB_APP_CLIENT_SECRET: process?.env.GITHUB_APP_CLIENT_SECRET!, + GITHUB_APP_WEBHOOK_SECRET: process?.env.GITHUB_APP_WEBHOOK_SECRET!, + REPO_TO_RUN_ON: + repoToRunOn == null || repoToRunOn === OMIT ? undefined : repoToRunOn, + REPO_DATA_S3_BUCKET: + repoDataS3Bucket == null || repoDataS3Bucket === OMIT + ? undefined + : repoDataS3Bucket, + REPO_DATA_S3_KEY: + repoDataS3Key == null || repoDataS3Key === OMIT + ? undefined + : repoDataS3Key, + DEFAULT_VENUS_ORIGIN: process?.env.DEFAULT_VENUS_ORIGIN!, + DEFAULT_FDR_ORIGIN: process?.env.DEFAULT_FDR_ORIGIN!, + FERNIE_SLACK_APP_TOKEN: process?.env.FERNIE_SLACK_APP_TOKEN!, + CUSTOMER_ALERTS_SLACK_CHANNEL: process?.env.CUSTOMER_ALERTS_SLACK_CHANNEL!, + CUSTOMER_PULLS_SLACK_CHANNEL: process?.env.CUSTOMER_PULLS_SLACK_CHANNEL!, + FERN_TOKEN: process?.env.FERN_TOKEN!, + ENVIRONMENT: process?.env.ENVIRONMENT!, + }; } diff --git a/servers/fern-bot/src/libs/fern.ts b/servers/fern-bot/src/libs/fern.ts index ba0e48355c..5656927d87 100644 --- a/servers/fern-bot/src/libs/fern.ts +++ b/servers/fern-bot/src/libs/fern.ts @@ -5,81 +5,96 @@ import path from "path"; import tmp from "tmp-promise"; import { doesPathExist } from "./fs"; -export async function execFernCli(command: string, cwd?: string, pipeYes: boolean = false): Promise { - console.log(`Running command on fern CLI: ${command}`); - const commandParts = command.split(" "); - try { - // Running the commands on Lambdas is a bit odd...specifically you can only write to tmp on a lambda - // so here we make sure the CLI is bundled via the `external` block in serverless.yml - // and then execute the command directly via node_modules, with the home and cache set to /tmp. - const tmpDir = await tmp.dir(); - const tmpDirPath = tmpDir.path; - process.env.NPM_CONFIG_CACHE = `${tmpDirPath}/.npm`; - process.env.HOME = tmpDirPath; +export async function execFernCli( + command: string, + cwd?: string, + pipeYes: boolean = false +): Promise { + console.log(`Running command on fern CLI: ${command}`); + const commandParts = command.split(" "); + try { + // Running the commands on Lambdas is a bit odd...specifically you can only write to tmp on a lambda + // so here we make sure the CLI is bundled via the `external` block in serverless.yml + // and then execute the command directly via node_modules, with the home and cache set to /tmp. + const tmpDir = await tmp.dir(); + const tmpDirPath = tmpDir.path; + process.env.NPM_CONFIG_CACHE = `${tmpDirPath}/.npm`; + process.env.HOME = tmpDirPath; - // Update config to allow `npm install` to work from within the `fern upgrade` command - process.env.NPM_CONFIG_PREFIX = tmpDirPath; - // Re-install the CLI to ensure it's at the correct path, given the updated config - await execa("npm", ["install", "-g", "fern-api"]); + // Update config to allow `npm install` to work from within the `fern upgrade` command + process.env.NPM_CONFIG_PREFIX = tmpDirPath; + // Re-install the CLI to ensure it's at the correct path, given the updated config + await execa("npm", ["install", "-g", "fern-api"]); - let command: Promise; - // If you don't have node_modules/fern-api, try using the CLI directly - if (!(await doesPathExist(`${process.cwd()}/node_modules/fern-api`))) { - // TODO: is there a better way to pipe `yes`? Piping the output of the real `yes` command doesn't work -- resulting in an EPIPE error. - command = execa("fern", commandParts, { - cwd, - input: pipeYes ? "y" : undefined, - }); - } else { - command = execa(`${process.cwd()}/node_modules/fern-api/cli.cjs`, commandParts, { - cwd, - input: pipeYes ? "y" : undefined, - }); + let command: Promise; + // If you don't have node_modules/fern-api, try using the CLI directly + if (!(await doesPathExist(`${process.cwd()}/node_modules/fern-api`))) { + // TODO: is there a better way to pipe `yes`? Piping the output of the real `yes` command doesn't work -- resulting in an EPIPE error. + command = execa("fern", commandParts, { + cwd, + input: pipeYes ? "y" : undefined, + }); + } else { + command = execa( + `${process.cwd()}/node_modules/fern-api/cli.cjs`, + commandParts, + { + cwd, + input: pipeYes ? "y" : undefined, } - return await command; - } catch (error) { - console.error("fern command failed."); - throw error; + ); } + return await command; + } catch (error) { + console.error("fern command failed."); + throw error; + } } // We pollute stdout with a version upgrade log, this tries to ignore that by only consuming the first line // Exported to leverage in tests export function cleanFernStdout(stdout: string): string { - return stdout.split("╭─")[0]?.split("\n")[0]?.trim() ?? ""; + return stdout.split("╭─")[0]?.split("\n")[0]?.trim() ?? ""; } // This type is meant to mirror the data model for the `generator list` command // defined in the OSS repo. type GeneratorList = Record>; export const NO_API_FALLBACK_KEY = "NO_API_FALLBACK"; -export async function getGenerators(fullRepoPath: string): Promise { - const tmpDir = await tmp.dir(); - const outputPath = `${tmpDir.path}/gen_list.yml`; - await execFernCli(`generator list --api-fallback ${NO_API_FALLBACK_KEY} -o ${outputPath}`, fullRepoPath); +export async function getGenerators( + fullRepoPath: string +): Promise { + const tmpDir = await tmp.dir(); + const outputPath = `${tmpDir.path}/gen_list.yml`; + await execFernCli( + `generator list --api-fallback ${NO_API_FALLBACK_KEY} -o ${outputPath}`, + fullRepoPath + ); - const data = await readFile(outputPath, "utf-8"); + const data = await readFile(outputPath, "utf-8"); - return yaml.load(data) as GeneratorList; + return yaml.load(data) as GeneratorList; } // Searches for a `fern.config.json` files within the repo, and returns the paths to them -export async function findFernWorkspaces(fullRepoPath: string): Promise { - async function searchDirectory(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }); - const results: string[] = []; +export async function findFernWorkspaces( + fullRepoPath: string +): Promise { + async function searchDirectory(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const results: string[] = []; - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...(await searchDirectory(fullPath))); - } else if (entry.name === "fern.config.json") { - results.push(path.dirname(fullPath)); - } - } - - return results; + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...(await searchDirectory(fullPath))); + } else if (entry.name === "fern.config.json") { + results.push(path.dirname(fullPath)); + } } - return searchDirectory(fullRepoPath); + return results; + } + + return searchDirectory(fullRepoPath); } diff --git a/servers/fern-bot/src/libs/fs.ts b/servers/fern-bot/src/libs/fs.ts index 3b56121b14..d18641db0a 100644 --- a/servers/fern-bot/src/libs/fs.ts +++ b/servers/fern-bot/src/libs/fs.ts @@ -2,19 +2,19 @@ import { lstatSync } from "fs"; import { lstat } from "fs/promises"; export async function doesPathExist(filepath: string): Promise { - try { - await lstat(filepath); - return true; - } catch { - return false; - } + try { + await lstat(filepath); + return true; + } catch { + return false; + } } export function doesPathExistSync(filepath: string): boolean { - try { - lstatSync(filepath); - return true; - } catch { - return false; - } + try { + lstatSync(filepath); + return true; + } catch { + return false; + } } diff --git a/servers/fern-bot/src/libs/github/octokit.ts b/servers/fern-bot/src/libs/github/octokit.ts index 3ca7b90764..ba31396556 100644 --- a/servers/fern-bot/src/libs/github/octokit.ts +++ b/servers/fern-bot/src/libs/github/octokit.ts @@ -2,10 +2,10 @@ import { Env } from "@libs/env"; import { App } from "octokit"; export const dashboard = async (app: App): Promise => { - const { data } = await app.octokit.request("GET /app"); + const { data } = await app.octokit.request("GET /app"); - return new Response( - ` + return new Response( + `

    GitHub App: ${data?.name}

    Installation count: ${data?.installations_count}

    @@ -20,25 +20,25 @@ export const dashboard = async (app: App): Promise => { source code

    `, - { - headers: { "content-type": "text/html" }, - }, - ); + { + headers: { "content-type": "text/html" }, + } + ); }; export const setupGithubApp = (env: Env): App => { - const app = new App({ - appId: env.GITHUB_APP_ID, - privateKey: env.GITHUB_APP_PRIVATE_KEY, - oauth: { - clientId: env.GITHUB_APP_CLIENT_ID, - clientSecret: env.GITHUB_APP_CLIENT_SECRET, - }, - webhooks: { - secret: env.GITHUB_APP_WEBHOOK_SECRET, - }, - }); + const app = new App({ + appId: env.GITHUB_APP_ID, + privateKey: env.GITHUB_APP_PRIVATE_KEY, + oauth: { + clientId: env.GITHUB_APP_CLIENT_ID, + clientSecret: env.GITHUB_APP_CLIENT_SECRET, + }, + webhooks: { + secret: env.GITHUB_APP_WEBHOOK_SECRET, + }, + }); - app.log.debug("Application loaded successfully."); - return app; + app.log.debug("Application loaded successfully."); + return app; }; diff --git a/servers/fern-bot/src/libs/github/octokitHooks.ts b/servers/fern-bot/src/libs/github/octokitHooks.ts index f1cdec9511..a0b838d4fb 100644 --- a/servers/fern-bot/src/libs/github/octokitHooks.ts +++ b/servers/fern-bot/src/libs/github/octokitHooks.ts @@ -2,58 +2,62 @@ import { Env } from "@libs/env"; import { App } from "octokit"; import { dashboard, setupGithubApp } from "./octokit"; -export async function handleIncomingRequest(request: Request, env: Env): Promise { - const application: App = setupGithubApp(env); - await webhooks(application); - - // display installation dashboard - if (request.method === "GET") { - return dashboard(application); - } - - // else verify webhook signature - try { - await verifySignature(application, request); - - return new Response("{ 'ok': true }", { - headers: { "content-type": "application/json" }, - }); - } catch (error) { - return new Response(`{ "error": "${error}" }`, { - status: 500, - headers: { "content-type": "application/json" }, - }); - } +export async function handleIncomingRequest( + request: Request, + env: Env +): Promise { + const application: App = setupGithubApp(env); + await webhooks(application); + + // display installation dashboard + if (request.method === "GET") { + return dashboard(application); + } + + // else verify webhook signature + try { + await verifySignature(application, request); + + return new Response("{ 'ok': true }", { + headers: { "content-type": "application/json" }, + }); + } catch (error) { + return new Response(`{ "error": "${error}" }`, { + status: 500, + headers: { "content-type": "application/json" }, + }); + } } const verifySignature = async (app: App, request: Request): Promise => { - const eventName = request.headers.get("x-github-event"); - if (eventName == null) { - throw new Error("Missing x-github-event header"); - } - await app.webhooks.verifyAndReceive({ - id: request.headers.get("x-github-delivery") ?? "x-github-delivery", - // @ts-expect-error: octokit does not export the type needed here to be able to cast - name: eventName, - signature: request.headers.get("x-hub-signature-256")?.replace(/sha256=/, "") ?? "", - payload: await request.text(), - }); + const eventName = request.headers.get("x-github-event"); + if (eventName == null) { + throw new Error("Missing x-github-event header"); + } + await app.webhooks.verifyAndReceive({ + id: request.headers.get("x-github-delivery") ?? "x-github-delivery", + // @ts-expect-error: octokit does not export the type needed here to be able to cast + name: eventName, + signature: + request.headers.get("x-hub-signature-256")?.replace(/sha256=/, "") ?? "", + payload: await request.text(), + }); }; const webhooks = async (app: App): Promise => { - app.log.info("Listening for issues.labeled webhooks"); - - app.webhooks.on("issues.labeled", async (context) => { - const label = context.payload.label; - app.log.info("Encountered labeled issue, noop", label); - - // // send post request using fetch to webhook - // await fetch(webhook, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(params), - // }); - }); + app.log.info("Listening for issues.labeled webhooks"); + + app.webhooks.on("issues.labeled", async (context) => { + const label = context.payload.label; + app.log.info("Encountered labeled issue, noop", label); + + // // send post request using fetch to webhook + // await fetch(webhook, { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify(params), + // }); + }); }; diff --git a/servers/fern-bot/src/libs/github/utilities.ts b/servers/fern-bot/src/libs/github/utilities.ts index 346a2f2c84..5314809a53 100644 --- a/servers/fern-bot/src/libs/github/utilities.ts +++ b/servers/fern-bot/src/libs/github/utilities.ts @@ -10,28 +10,42 @@ export const DEFAULT_REMOTE_NAME = "origin"; export type Repository = components["schemas"]["repository"]; export type PullRequest = components["schemas"]["pull-request"]; -export async function configureGit(repository: Repository): Promise<[SimpleGit, string]> { - const tmpDir = await tmp.dir(); - const fullRepoPath = path.join(tmpDir.path, repository.id.toString(), repository.name); - if (!(await doesPathExist(fullRepoPath))) { - await mkdir(fullRepoPath, { recursive: true }); - } - return [simpleGit(fullRepoPath), fullRepoPath]; +export async function configureGit( + repository: Repository +): Promise<[SimpleGit, string]> { + const tmpDir = await tmp.dir(); + const fullRepoPath = path.join( + tmpDir.path, + repository.id.toString(), + repository.name + ); + if (!(await doesPathExist(fullRepoPath))) { + await mkdir(fullRepoPath, { recursive: true }); + } + return [simpleGit(fullRepoPath), fullRepoPath]; } export async function cloneRepo( - git: SimpleGit, - repository: Repository, - octokit: Octokit, - fernBotLoginName: string, - fernBotLoginId: string, + git: SimpleGit, + repository: Repository, + octokit: Octokit, + fernBotLoginName: string, + fernBotLoginId: string ): Promise { - const installationToken = ((await octokit.auth({ type: "installation" })) as any).token; + const installationToken = ( + (await octokit.auth({ type: "installation" })) as any + ).token; - const authedCloneUrl = repository.clone_url.replace("https://", `https://x-access-token:${installationToken}@`); - // Clone the repo to fullRepoPath and update the branch - await git.clone(authedCloneUrl, "."); - // Configure git to show the app as the committer - await git.addConfig("user.name", fernBotLoginName); - await git.addConfig("user.email", `${fernBotLoginId}+${fernBotLoginName}@users.noreply.github.com`); + const authedCloneUrl = repository.clone_url.replace( + "https://", + `https://x-access-token:${installationToken}@` + ); + // Clone the repo to fullRepoPath and update the branch + await git.clone(authedCloneUrl, "."); + // Configure git to show the app as the committer + await git.addConfig("user.name", fernBotLoginName); + await git.addConfig( + "user.email", + `${fernBotLoginId}+${fernBotLoginName}@users.noreply.github.com` + ); } diff --git a/servers/fern-bot/src/libs/handler-wrapper.ts b/servers/fern-bot/src/libs/handler-wrapper.ts index f70a5877e1..7c29876ce1 100644 --- a/servers/fern-bot/src/libs/handler-wrapper.ts +++ b/servers/fern-bot/src/libs/handler-wrapper.ts @@ -1,48 +1,60 @@ -import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda"; +import type { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from "aws-lambda"; export const handlerWrapper = - (handlerFunction: (event: APIGatewayProxyEvent, context: Context) => Promise) => - async (event: APIGatewayProxyEvent, context: Context): Promise => { - context.callbackWaitsForEmptyEventLoop = false; + ( + handlerFunction: ( + event: APIGatewayProxyEvent, + context: Context + ) => Promise + ) => + async ( + event: APIGatewayProxyEvent, + context: Context + ): Promise => { + context.callbackWaitsForEmptyEventLoop = false; - if (event.httpMethod === "OPTIONS") { - return { - statusCode: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE", - "Access-Control-Allow-Headers": - "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", - "Access-Control-Allow-Credentials": true, - }, - body: "", - }; - } + if (event.httpMethod === "OPTIONS") { + return { + statusCode: 200, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE", + "Access-Control-Allow-Headers": + "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", + "Access-Control-Allow-Credentials": true, + }, + body: "", + }; + } - try { - const response = await handlerFunction(event, context); + try { + const response = await handlerFunction(event, context); - return { - statusCode: 200, - body: JSON.stringify(response), - headers: { - "Access-Control-Allow-Origin": "*", // Required for CORS support to work - "Access-Control-Allow-Credentials": true, // Required for cookies, authorization headers with HTTPS - }, - }; - } catch (err) { - if (err instanceof Error) { - console.error(err); - return { statusCode: 500, body: err.message }; - } + return { + statusCode: 200, + body: JSON.stringify(response), + headers: { + "Access-Control-Allow-Origin": "*", // Required for CORS support to work + "Access-Control-Allow-Credentials": true, // Required for cookies, authorization headers with HTTPS + }, + }; + } catch (err) { + if (err instanceof Error) { + console.error(err); + return { statusCode: 500, body: err.message }; + } - return { - statusCode: 500, - body: "Unexpected Error", - headers: { - "Access-Control-Allow-Origin": "*", // Required for CORS support to work - "Access-Control-Allow-Credentials": true, // Required for cookies, authorization headers with HTTPS - }, - }; - } - }; + return { + statusCode: 500, + body: "Unexpected Error", + headers: { + "Access-Control-Allow-Origin": "*", // Required for CORS support to work + "Access-Control-Allow-Credentials": true, // Required for cookies, authorization headers with HTTPS + }, + }; + } + }; diff --git a/servers/fern-bot/src/libs/schemas/RepoData.ts b/servers/fern-bot/src/libs/schemas/RepoData.ts index 6764216a62..7571b16f1c 100644 --- a/servers/fern-bot/src/libs/schemas/RepoData.ts +++ b/servers/fern-bot/src/libs/schemas/RepoData.ts @@ -1,7 +1,7 @@ export interface RepoData { - id: string; - name: string; - full_name: string; - default_branch: string; - clone_url: string; + id: string; + name: string; + full_name: string; + default_branch: string; + clone_url: string; } diff --git a/servers/fern-bot/src/libs/slack/SlackService.ts b/servers/fern-bot/src/libs/slack/SlackService.ts index e64d8190a1..4e4dce2521 100644 --- a/servers/fern-bot/src/libs/slack/SlackService.ts +++ b/servers/fern-bot/src/libs/slack/SlackService.ts @@ -2,364 +2,385 @@ import { PullRequest } from "@fern-fern/paged-generators-sdk/api"; import { KnownBlock, SectionBlock, WebClient } from "@slack/web-api"; export interface GeneratorMessageMetadata { - group: string; - generatorName: string; - apiName?: string; + group: string; + generatorName: string; + apiName?: string; } export class SlackService { - private slackClient: WebClient; + private slackClient: WebClient; - constructor( - slackToken: string, - private readonly slackChannel: string, - ) { - this.slackClient = new WebClient(slackToken); - } + constructor( + slackToken: string, + private readonly slackChannel: string + ) { + this.slackClient = new WebClient(slackToken); + } + + private getGeneratorMetadataMessage( + generator: GeneratorMessageMetadata + ): string { + return `*Generator Group:* ${generator.group}\n*Generator:* ${generator.generatorName}${generator.apiName ? `\n*API:* ${generator.apiName}` : ""}`; + } - private getGeneratorMetadataMessage(generator: GeneratorMessageMetadata): string { - return `*Generator Group:* ${generator.group}\n*Generator:* ${generator.generatorName}${generator.apiName ? `\n*API:* ${generator.apiName}` : ""}`; + // TODO: would be nice if we stored customer metadata in a DB to then add that information to this message + private addContextToHeader( + header: string, + maybeOrganization: string | undefined, + generatorName?: string + ): string { + if (generatorName == null) { + return `:fern: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for Fern CLI`; } - // TODO: would be nice if we stored customer metadata in a DB to then add that information to this message - private addContextToHeader(header: string, maybeOrganization: string | undefined, generatorName?: string): string { - if (generatorName == null) { - return `:fern: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for Fern CLI`; - } + if (generatorName.includes("python")) { + return `:python: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("typescript")) { + return `:ts: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("java")) { + return `:java: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("ruby")) { + return `:ruby: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("csharp")) { + return `:csharp: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("go")) { + return `:gopher: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("php")) { + return `:php: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; + } - if (generatorName.includes("python")) { - return `:python: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } else if (generatorName.includes("typescript")) { - return `:ts: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } else if (generatorName.includes("java")) { - return `:java: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } else if (generatorName.includes("ruby")) { - return `:ruby: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } else if (generatorName.includes("csharp")) { - return `:csharp: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } else if (generatorName.includes("go")) { - return `:gopher: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } else if (generatorName.includes("php")) { - return `:php: ${this.getOrganizationName(maybeOrganization, true)} - ${header} for \`${generatorName}\``; - } + return `:interrobang: ${header} for \`${generatorName}\``; + } - return `:interrobang: ${header} for \`${generatorName}\``; + getOrganizationName( + organization: string | undefined, + inTitle?: boolean + ): string { + if (inTitle) { + return organization ? organization : "No Org"; + } else { + return organization + ? organization + : "Org Not Found (you may need that CLI upgrade)"; } + } - getOrganizationName(organization: string | undefined, inTitle?: boolean): string { - if (inTitle) { - return organization ? organization : "No Org"; - } else { - return organization ? organization : "Org Not Found (you may need that CLI upgrade)"; - } - } + public async notifyStaleTeamPulls({ + numStaleTeamPulls, + retoolLink, + }: { + numStaleTeamPulls: number; + retoolLink: string; + }): Promise { + await this.slackClient.chat.postMessage({ + channel: this.slackChannel, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: `:sleeping: :fernie: There are ${numStaleTeamPulls} stale PRs opened by Fern team members`, + }, + }, + { + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View in Retool :control_knobs:", + }, + url: retoolLink, + }, + ], + }, + ], + }); + } - public async notifyStaleTeamPulls({ - numStaleTeamPulls, - retoolLink, - }: { - numStaleTeamPulls: number; - retoolLink: string; - }): Promise { - await this.slackClient.chat.postMessage({ - channel: this.slackChannel, - blocks: [ - { - type: "header", - text: { - type: "plain_text", - text: `:sleeping: :fernie: There are ${numStaleTeamPulls} stale PRs opened by Fern team members`, - }, - }, - { - type: "actions", - block_id: "actions1", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: "View in Retool :control_knobs:", - }, - url: retoolLink, - }, - ], - }, - ], - }); + // :sleeping: has stale upgrade PRs + // Organization: courier + // Github Repo: trycourier/courier-api + // API Spec Update PR: #123 + // Version Update PRs: #124, #125, [...] (overflow) + // [View in Retool ] + // + // OR if there are too many + // + // :sleeping: has 10 stale upgrade PRs + // [View Pulls ] [View in Retool ] + public async notifyStaleUpgradePRs({ + versionUpdatePulls, + apiSpecPull, + organization, + repoName, + retoolLink, + shouldBeVerbose, + }: { + organization: string; + repoName: string; + versionUpdatePulls: PullRequest[]; + apiSpecPull?: PullRequest; + retoolLink: string; + shouldBeVerbose: boolean; + }): Promise { + const verboseBlocks: KnownBlock[] = [ + { + type: "header", + text: { + type: "plain_text", + text: `:sleeping: ${organization} has stale upgrade PRs`, + }, + }, + { + type: "divider", + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Organization*: ${this.getOrganizationName(organization)}\n*Github Repo:* ${repoName}${this.getStaleAPISpecPRMessage(apiSpecPull)}`, + }, + }, + ]; + + const maybeVersionUpdateMessage = + this.getStaleVersionUpgradePRsMessage(versionUpdatePulls); + if (maybeVersionUpdateMessage != null) { + verboseBlocks.push(maybeVersionUpdateMessage); } - // :sleeping: has stale upgrade PRs - // Organization: courier - // Github Repo: trycourier/courier-api - // API Spec Update PR: #123 - // Version Update PRs: #124, #125, [...] (overflow) - // [View in Retool ] - // - // OR if there are too many + verboseBlocks.push({ + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View in Retool :control_knobs:", + }, + url: retoolLink, + }, + ], + }); + + const allPulls = versionUpdatePulls.concat( + apiSpecPull ? [apiSpecPull] : [] + ); + const aPull = allPulls[0]; + // TODO: we should maybe add the repo to the pull, or at least this link so we don't + // have to do this sketchy string replace, but rather have the pulls URL on the pull + // NOTE: The pulls URL is not the link to the specific PR, but the link to the PRs page // - // :sleeping: has 10 stale upgrade PRs - // [View Pulls ] [View in Retool ] - public async notifyStaleUpgradePRs({ - versionUpdatePulls, - apiSpecPull, - organization, - repoName, - retoolLink, - shouldBeVerbose, - }: { - organization: string; - repoName: string; - versionUpdatePulls: PullRequest[]; - apiSpecPull?: PullRequest; - retoolLink: string; - shouldBeVerbose: boolean; - }): Promise { - const verboseBlocks: KnownBlock[] = [ + // We only call this function if there's at least one PR so this should be safe + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const pullsLink = aPull!.url.replace( + aPull!.pullRequestNumber.toString(), + "" + ); + await this.slackClient.chat.postMessage({ + channel: this.slackChannel, + blocks: shouldBeVerbose + ? verboseBlocks + : [ { - type: "header", - text: { - type: "plain_text", - text: `:sleeping: ${organization} has stale upgrade PRs`, - }, - }, - { - type: "divider", + type: "header", + text: { + type: "plain_text", + text: `:sleeping: ${organization} has ${versionUpdatePulls.length} stale upgrade PRs ${apiSpecPull ? "and a stale API Spec PR" : ""}`, + }, }, { - type: "section", - text: { - type: "mrkdwn", - text: `*Organization*: ${this.getOrganizationName(organization)}\n*Github Repo:* ${repoName}${this.getStaleAPISpecPRMessage(apiSpecPull)}`, + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View Pulls :link:", + }, + url: pullsLink, }, - }, - ]; - - const maybeVersionUpdateMessage = this.getStaleVersionUpgradePRsMessage(versionUpdatePulls); - if (maybeVersionUpdateMessage != null) { - verboseBlocks.push(maybeVersionUpdateMessage); - } - - verboseBlocks.push({ - type: "actions", - block_id: "actions1", - elements: [ { - type: "button", - text: { - type: "plain_text", - text: "View in Retool :control_knobs:", - }, - url: retoolLink, + type: "button", + text: { + type: "plain_text", + text: "View in Retool :control_knobs:", + }, + url: retoolLink, }, - ], - }); + ], + }, + ], + }); + } - const allPulls = versionUpdatePulls.concat(apiSpecPull ? [apiSpecPull] : []); - const aPull = allPulls[0]; - // TODO: we should maybe add the repo to the pull, or at least this link so we don't - // have to do this sketchy string replace, but rather have the pulls URL on the pull - // NOTE: The pulls URL is not the link to the specific PR, but the link to the PRs page - // - // We only call this function if there's at least one PR so this should be safe - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const pullsLink = aPull!.url.replace(aPull!.pullRequestNumber.toString(), ""); - await this.slackClient.chat.postMessage({ - channel: this.slackChannel, - blocks: shouldBeVerbose - ? verboseBlocks - : [ - { - type: "header", - text: { - type: "plain_text", - text: `:sleeping: ${organization} has ${versionUpdatePulls.length} stale upgrade PRs ${apiSpecPull ? "and a stale API Spec PR" : ""}`, - }, - }, - { - type: "actions", - block_id: "actions1", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: "View Pulls :link:", - }, - url: pullsLink, - }, - { - type: "button", - text: { - type: "plain_text", - text: "View in Retool :control_knobs:", - }, - url: retoolLink, - }, - ], - }, - ], - }); - } + getStaleAPISpecPRMessage(apiSpecPull: PullRequest | undefined): string { + return apiSpecPull + ? `\n*API Spec Update PR:* ${this.linkPR(apiSpecPull)}` + : ""; + } - getStaleAPISpecPRMessage(apiSpecPull: PullRequest | undefined): string { - return apiSpecPull ? `\n*API Spec Update PR:* ${this.linkPR(apiSpecPull)}` : ""; - } + getStaleVersionUpgradePRsMessage( + versionUpdatePulls: PullRequest[] + ): SectionBlock | undefined { + if (versionUpdatePulls.length > 0) { + let message = "*Version Update PRs:*"; + // Inline the first 3 PRs + for (const pull of versionUpdatePulls.slice(2)) { + message += ` ${this.linkPR(pull)}`; + } - getStaleVersionUpgradePRsMessage(versionUpdatePulls: PullRequest[]): SectionBlock | undefined { - if (versionUpdatePulls.length > 0) { - let message = "*Version Update PRs:*"; - // Inline the first 3 PRs - for (const pull of versionUpdatePulls.slice(2)) { - message += ` ${this.linkPR(pull)}`; - } - - if (versionUpdatePulls.length > 3) { - return { - type: "section", - text: { - type: "mrkdwn", - text: message, - }, - accessory: { - type: "overflow", - // Slack limits options to 5, so only going up to 8 total PRs linked, including the inlined ones - options: versionUpdatePulls.slice(3, 8).map((pull) => ({ - text: { - type: "plain_text", - text: pull.title, - }, - description: { - type: "plain_text", - text: `#${pull.pullRequestNumber}`, - }, - url: pull.url, - value: "placeholder", - })), - action_id: "overflow_prs", - }, - }; - } - return { - type: "section", - text: { - type: "mrkdwn", - text: message, - }, - }; - } - return; + if (versionUpdatePulls.length > 3) { + return { + type: "section", + text: { + type: "mrkdwn", + text: message, + }, + accessory: { + type: "overflow", + // Slack limits options to 5, so only going up to 8 total PRs linked, including the inlined ones + options: versionUpdatePulls.slice(3, 8).map((pull) => ({ + text: { + type: "plain_text", + text: pull.title, + }, + description: { + type: "plain_text", + text: `#${pull.pullRequestNumber}`, + }, + url: pull.url, + value: "placeholder", + })), + action_id: "overflow_prs", + }, + }; + } + return { + type: "section", + text: { + type: "mrkdwn", + text: message, + }, + }; } + return; + } - linkPR(pull: PullRequest): string { - return `<${pull.url}|#${pull.pullRequestNumber}>`; - } + linkPR(pull: PullRequest): string { + return `<${pull.url}|#${pull.pullRequestNumber}>`; + } - public async notifyUpgradePRCreated({ - fromVersion, - toVersion, - prUrl, - repoName, - generator, - maybeOrganization, - }: { - fromVersion: string; - toVersion: string; - prUrl: string; - repoName: string; - generator?: GeneratorMessageMetadata; - maybeOrganization: string | undefined; - }): Promise { - await this.slackClient.chat.postMessage({ - channel: this.slackChannel, - blocks: [ - { - type: "header", - text: { - type: "plain_text", - text: this.addContextToHeader( - "Upgrade PR Created", - maybeOrganization, - generator?.generatorName, - ), - }, - }, - { - type: "divider", - }, - { - type: "section", - text: { - type: "mrkdwn", - text: `*Organization*: ${this.getOrganizationName(maybeOrganization)}\n*Github Repo:* ${repoName}${generator ? "\n" + this.getGeneratorMetadataMessage(generator) : ""}\n*Upgrading:* ${fromVersion} :arrow_right: ${toVersion}`, - }, - }, - { - type: "actions", - block_id: "actions1", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: "View PR :link:", - }, - url: prUrl, - }, - ], - }, - ], - }); - } + public async notifyUpgradePRCreated({ + fromVersion, + toVersion, + prUrl, + repoName, + generator, + maybeOrganization, + }: { + fromVersion: string; + toVersion: string; + prUrl: string; + repoName: string; + generator?: GeneratorMessageMetadata; + maybeOrganization: string | undefined; + }): Promise { + await this.slackClient.chat.postMessage({ + channel: this.slackChannel, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: this.addContextToHeader( + "Upgrade PR Created", + maybeOrganization, + generator?.generatorName + ), + }, + }, + { + type: "divider", + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Organization*: ${this.getOrganizationName(maybeOrganization)}\n*Github Repo:* ${repoName}${generator ? "\n" + this.getGeneratorMetadataMessage(generator) : ""}\n*Upgrading:* ${fromVersion} :arrow_right: ${toVersion}`, + }, + }, + { + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View PR :link:", + }, + url: prUrl, + }, + ], + }, + ], + }); + } - public async notifyMajorVersionUpgradeEncountered({ - repoUrl, - repoName, - currentVersion, - generator, - maybeOrganization, - }: { - repoUrl: string; - repoName: string; - currentVersion: string; - generator?: GeneratorMessageMetadata; - maybeOrganization: string | undefined; - }): Promise { - await this.slackClient.chat.postMessage({ - channel: this.slackChannel, - blocks: [ - { - type: "header", - text: { - type: "plain_text", - text: `:rotating_light: ${this.addContextToHeader("Major version upgrade encountered", maybeOrganization, generator?.generatorName)}`, - }, - }, - { - type: "divider", - }, - { - type: "section", - text: { - type: "mrkdwn", - text: `Hey , we've encountered a major version upgrade which needs manual intervention!\n\n*Organization*: ${this.getOrganizationName(maybeOrganization)}\n*Github Repo*: ${repoName}${generator ? "\n" + this.getGeneratorMetadataMessage(generator) : ""}\n*Current version*: ${currentVersion}`, - }, - }, - { - type: "actions", - block_id: "actions1", - elements: [ - { - type: "button", - text: { - type: "plain_text", - text: "Visit Repo :link:", - }, - url: repoUrl, - }, - ], - }, - ], - }); - } + public async notifyMajorVersionUpgradeEncountered({ + repoUrl, + repoName, + currentVersion, + generator, + maybeOrganization, + }: { + repoUrl: string; + repoName: string; + currentVersion: string; + generator?: GeneratorMessageMetadata; + maybeOrganization: string | undefined; + }): Promise { + await this.slackClient.chat.postMessage({ + channel: this.slackChannel, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: `:rotating_light: ${this.addContextToHeader("Major version upgrade encountered", maybeOrganization, generator?.generatorName)}`, + }, + }, + { + type: "divider", + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `Hey , we've encountered a major version upgrade which needs manual intervention!\n\n*Organization*: ${this.getOrganizationName(maybeOrganization)}\n*Github Repo*: ${repoName}${generator ? "\n" + this.getGeneratorMetadataMessage(generator) : ""}\n*Current version*: ${currentVersion}`, + }, + }, + { + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Visit Repo :link:", + }, + url: repoUrl, + }, + ], + }, + ], + }); + } } diff --git a/servers/fern-bot/src/utils/createLoggingExecutable.ts b/servers/fern-bot/src/utils/createLoggingExecutable.ts index ac50a43e20..1cd624978d 100644 --- a/servers/fern-bot/src/utils/createLoggingExecutable.ts +++ b/servers/fern-bot/src/utils/createLoggingExecutable.ts @@ -1,15 +1,22 @@ import { Options, Result } from "execa"; import { loggingExeca } from "./loggingExeca"; -export type LoggingExecutable = (args?: string[], options?: loggingExeca.Options) => Promise; +export type LoggingExecutable = ( + args?: string[], + options?: loggingExeca.Options +) => Promise; export declare namespace createLoggingExecutable { - export interface Options extends loggingExeca.Options {} + export interface Options extends loggingExeca.Options {} } export function createLoggingExecutable( - executable: string, - { ...loggingExecaOptions }: createLoggingExecutable.Options = {}, + executable: string, + { ...loggingExecaOptions }: createLoggingExecutable.Options = {} ): LoggingExecutable { - return (args, commandOptions) => loggingExeca(executable, args, { ...loggingExecaOptions, ...commandOptions }); + return (args, commandOptions) => + loggingExeca(executable, args, { + ...loggingExecaOptions, + ...commandOptions, + }); } diff --git a/servers/fern-bot/src/utils/fetchAndUnzip.ts b/servers/fern-bot/src/utils/fetchAndUnzip.ts index 11833f7e65..6e9e58df3b 100644 --- a/servers/fern-bot/src/utils/fetchAndUnzip.ts +++ b/servers/fern-bot/src/utils/fetchAndUnzip.ts @@ -7,56 +7,65 @@ import tmp from "tmp-promise"; import { promisify } from "util"; export interface FetchAndUnzipRequest { - destination: string; - sourceUrl: string; + destination: string; + sourceUrl: string; } const ZIP_FILENAME = "source.zip"; -export async function fetchAndUnzip(request: FetchAndUnzipRequest): Promise { - const destinationPath = path.join((await tmp.dir()).path, ZIP_FILENAME); - await downloadFile({ - sourceUrl: request.sourceUrl, - destinationPath, - }); +export async function fetchAndUnzip( + request: FetchAndUnzipRequest +): Promise { + const destinationPath = path.join((await tmp.dir()).path, ZIP_FILENAME); + await downloadFile({ + sourceUrl: request.sourceUrl, + destinationPath, + }); - console.debug(`Unzipping source from ${destinationPath} to ${request.destination}`); - await unzipFile(destinationPath, request.destination); + console.debug( + `Unzipping source from ${destinationPath} to ${request.destination}` + ); + await unzipFile(destinationPath, request.destination); - console.debug(`Removing ${destinationPath}`); - await unlink(destinationPath); + console.debug(`Removing ${destinationPath}`); + await unlink(destinationPath); } -async function unzipFile(sourcePath: string, destinationPath: string): Promise { - return new Promise((resolve, reject) => { - try { - const zip = new AdmZip(sourcePath); - - // Extract all files - zip.extractAllTo(destinationPath, true); - - console.log(`Successfully extracted ${sourcePath} to ${destinationPath}`); - resolve(); - } catch (error) { - console.error(`Error unzipping file: ${error}`); - reject(error as Error); - } - }); +async function unzipFile( + sourcePath: string, + destinationPath: string +): Promise { + return new Promise((resolve, reject) => { + try { + const zip = new AdmZip(sourcePath); + + // Extract all files + zip.extractAllTo(destinationPath, true); + + console.log(`Successfully extracted ${sourcePath} to ${destinationPath}`); + resolve(); + } catch (error) { + console.error(`Error unzipping file: ${error}`); + reject(error as Error); + } + }); } async function downloadFile({ - sourceUrl, - destinationPath, + sourceUrl, + destinationPath, }: { - sourceUrl: string; - destinationPath: string; + sourceUrl: string; + destinationPath: string; }): Promise { - console.debug(`Downloading ${sourceUrl} to ${destinationPath}`); - const response = await fetch(sourceUrl); - if (!response.ok) { - throw new Error(`Failed to download source. Status: ${response.status}, ${response.statusText}`); - } - const fileStream = createWriteStream(destinationPath); + console.debug(`Downloading ${sourceUrl} to ${destinationPath}`); + const response = await fetch(sourceUrl); + if (!response.ok) { + throw new Error( + `Failed to download source. Status: ${response.status}, ${response.statusText}` + ); + } + const fileStream = createWriteStream(destinationPath); - await promisify(pipeline)(response.body as any, fileStream); + await promisify(pipeline)(response.body as any, fileStream); } diff --git a/servers/fern-bot/src/utils/loggingExeca.ts b/servers/fern-bot/src/utils/loggingExeca.ts index dc303187ee..019cf01727 100644 --- a/servers/fern-bot/src/utils/loggingExeca.ts +++ b/servers/fern-bot/src/utils/loggingExeca.ts @@ -1,36 +1,43 @@ import { execa, Options as ExecaOptions, Result } from "execa"; export declare namespace loggingExeca { - export interface Options extends ExecaOptions { - doNotPipeOutput?: boolean; - secrets?: string[]; - substitutions?: Record; - } + export interface Options extends ExecaOptions { + doNotPipeOutput?: boolean; + secrets?: string[]; + substitutions?: Record; + } } export async function loggingExeca( - executable: string, - args: string[] = [], - { doNotPipeOutput = false, secrets = [], substitutions = {}, ...execaOptions }: loggingExeca.Options = {}, + executable: string, + args: string[] = [], + { + doNotPipeOutput = false, + secrets = [], + substitutions = {}, + ...execaOptions + }: loggingExeca.Options = {} ): Promise { - const allSubstitutions = secrets.reduce( - (acc, secret) => ({ - ...acc, - [secret]: "", - }), - substitutions, - ); + const allSubstitutions = secrets.reduce( + (acc, secret) => ({ + ...acc, + [secret]: "", + }), + substitutions + ); - let logLine = [executable, ...args].join(" "); - for (const [substitutionKey, substitutionValue] of Object.entries(allSubstitutions)) { - logLine = logLine.replaceAll(substitutionKey, substitutionValue); - } + let logLine = [executable, ...args].join(" "); + for (const [substitutionKey, substitutionValue] of Object.entries( + allSubstitutions + )) { + logLine = logLine.replaceAll(substitutionKey, substitutionValue); + } - console.log(`+ ${logLine}`); - const command = execa(executable, args, execaOptions); - if (!doNotPipeOutput) { - command.stdout?.pipe(process.stdout); - command.stderr?.pipe(process.stderr); - } - return command; + console.log(`+ ${logLine}`); + const command = execa(executable, args, execaOptions); + if (!doNotPipeOutput) { + command.stdout?.pipe(process.stdout); + command.stderr?.pipe(process.stderr); + } + return command; } diff --git a/servers/fern-bot/tsconfig.eslint.json b/servers/fern-bot/tsconfig.eslint.json index 62de7250d7..21b279e5dc 100644 --- a/servers/fern-bot/tsconfig.eslint.json +++ b/servers/fern-bot/tsconfig.eslint.json @@ -5,5 +5,11 @@ "noEmit": true }, "include": ["src/**/*", "vitest.config.ts", "lib/**/*"], - "exclude": ["node_modules/**/*", ".serverless/**/*", ".webpack/**/*", "_warmup/**/*", ".vscode/**/*"] + "exclude": [ + "node_modules/**/*", + ".serverless/**/*", + ".webpack/**/*", + "_warmup/**/*", + ".vscode/**/*" + ] } diff --git a/servers/fern-bot/vitest.config.ts b/servers/fern-bot/vitest.config.ts index b051f71d46..ebb7c1f6a5 100644 --- a/servers/fern-bot/vitest.config.ts +++ b/servers/fern-bot/vitest.config.ts @@ -1,13 +1,13 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - alias: { - "@functions/": new URL("src/functions/", import.meta.url).pathname, - "@generated/": new URL("src/generated/", import.meta.url).pathname, - "@libs/": new URL("src/libs/", import.meta.url).pathname, - "@utils/": new URL("src/utils/", import.meta.url).pathname, - }, + test: { + globals: true, + alias: { + "@functions/": new URL("src/functions/", import.meta.url).pathname, + "@generated/": new URL("src/generated/", import.meta.url).pathname, + "@libs/": new URL("src/libs/", import.meta.url).pathname, + "@utils/": new URL("src/utils/", import.meta.url).pathname, }, + }, });