From 6479103bd7772dcba446f3d5343c2555c2550ea4 Mon Sep 17 00:00:00 2001 From: Rohin Bhargava Date: Thu, 19 Dec 2024 11:39:19 -0500 Subject: [PATCH] Revert "Revert "chore: modify fdr apis to accomodate openapi parser for docs generation"" (#1914) --- .../fdr/definition/api/latest/__package__.yml | 19 ++ .../api/v1/register/__package__.yml | 4 +- .../definition/docs/v1/read/__package__.yml | 2 + .../src/definition-object-factory.ts | 1 + .../api/resources/api/client/Client.ts | 7 + .../api/resources/latest/client/Client.ts | 92 ++++++++ .../resources/latest/client/getApiLatest.ts | 53 +++++ .../api/resources/latest/client/index.ts | 1 + .../resources/api/resources/latest/index.ts | 1 + .../v1/resources/register/client/Client.ts | 200 +++++++++++++++++ .../requests/RegisterApiDefinitionRequest.ts | 203 +++++++++++++++++- .../v1/resources/read/types/DocsDefinition.ts | 1 + .../read/convertDocsDefinitionToRead.ts | 3 + .../fdr-sdk/src/navigation/utils/toApis.ts | 17 +- packages/parsers/.gitignore | 24 +-- .../parsers/src/client/generated/.fernignore | 2 + .../resources/api/resources/latest/index.ts | 2 +- .../v1/resources/read/types/DocsDefinition.ts | 1 + packages/parsers/tsconfig.json | 23 +- .../ui/app/src/resolver/resolveDocsContent.ts | 31 ++- .../ui/docs-bundle/src/server/DocsLoader.ts | 6 +- .../src/server/withResolvedDocsContent.ts | 1 + .../src/algolia/__test__/test-utils.ts | 27 +-- .../src/fdr/load-docs-with-url.ts | 17 +- .../src/ApiDefinitionLoader.ts | 22 +- .../src/utils/getDocsPageProps.ts | 1 + .../migration.sql | 10 + servers/fdr/prisma/schema.prisma | 10 + .../src/__test__/local/services/api.test.ts | 63 +++++- .../fdr/src/__test__/local/setupMockFdr.ts | 2 + servers/fdr/src/__test__/local/util.ts | 44 +++- .../latest/errors/ApiDoesNotExistError.d.ts | 9 + .../latest/errors/ApiDoesNotExistError.js | 24 +++ .../api/resources/latest/errors/index.d.ts | 1 + .../api/resources/latest/errors/index.js | 1 + .../resources/api/resources/latest/index.d.ts | 2 + .../resources/api/resources/latest/index.js | 2 + .../latest/service/LatestService.d.ts | 21 ++ .../resources/latest/service/LatestService.js | 58 +++++ .../api/resources/latest/service/index.d.ts | 1 + .../api/resources/latest/service/index.js | 1 + .../RegisterApiDefinitionRequest.d.ts | 3 +- .../resources/read/types/DocsDefinition.d.ts | 1 + servers/fdr/src/api/generated/register.d.ts | 4 + servers/fdr/src/api/generated/register.js | 1 + servers/fdr/src/api/index.ts | 1 + .../controllers/api/getApiLatestService.ts | 35 +++ .../controllers/api/getRegisterApiService.ts | 136 +++++++----- .../controllers/docs/v1/getDocsReadService.ts | 15 +- .../docs/v2/getDocsWriteV2Service.ts | 43 +++- servers/fdr/src/db/api/APIDefinitionDao.ts | 21 +- servers/fdr/src/healthchecks/checkRedis.ts | 1 + servers/fdr/src/server.ts | 4 + .../fdr/src/services/db/DatabaseService.ts | 20 +- .../docs-cache/DocsDefinitionCache.ts | 2 +- 55 files changed, 1145 insertions(+), 152 deletions(-) create mode 100644 packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/Client.ts create mode 100644 packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/getApiLatest.ts create mode 100644 packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/index.ts create mode 100644 servers/fdr/prisma/migrations/20241217171736_add_api_definitions_latest/migration.sql create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.d.ts create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.js create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.d.ts create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.js create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.d.ts create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.js create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.d.ts create mode 100644 servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.js create mode 100644 servers/fdr/src/controllers/api/getApiLatestService.ts diff --git a/fern/apis/fdr/definition/api/latest/__package__.yml b/fern/apis/fdr/definition/api/latest/__package__.yml index 75a26d8d8f..adb2944846 100644 --- a/fern/apis/fdr/definition/api/latest/__package__.yml +++ b/fern/apis/fdr/definition/api/latest/__package__.yml @@ -9,6 +9,21 @@ imports: rootCommons: ../../commons.yml auth: auth.yml +service: + base-path: /registry/api/latest + auth: true + audiences: + - read + endpoints: + getApiLatest: + method: GET + path: /load/{apiDefinitionId} + path-parameters: + apiDefinitionId: rootCommons.ApiDefinitionId + response: ApiDefinition + errors: + - ApiDoesNotExistError + types: ApiDefinition: properties: @@ -20,3 +35,7 @@ types: subpackages: map auths: map globalHeaders: optional> + +errors: + ApiDoesNotExistError: + status-code: 404 diff --git a/fern/apis/fdr/definition/api/v1/register/__package__.yml b/fern/apis/fdr/definition/api/v1/register/__package__.yml index 660c54ce10..456b427bf5 100644 --- a/fern/apis/fdr/definition/api/v1/register/__package__.yml +++ b/fern/apis/fdr/definition/api/v1/register/__package__.yml @@ -5,6 +5,7 @@ imports: type: type.yml rootCommons: ../../../commons.yml commons: ../commons.yml + latest: ../../latest/__package__.yml webhook: webhook.yml websocket: websocket.yml @@ -23,7 +24,8 @@ service: properties: orgId: rootCommons.OrgId apiId: rootCommons.ApiId - definition: ApiDefinition + definition: optional + definitionV2: optional sources: optional> response: RegisterApiDefinitionResponse errors: diff --git a/fern/apis/fdr/definition/docs/v1/read/__package__.yml b/fern/apis/fdr/definition/docs/v1/read/__package__.yml index 3c726e90a3..f9ad0af875 100644 --- a/fern/apis/fdr/definition/docs/v1/read/__package__.yml +++ b/fern/apis/fdr/definition/docs/v1/read/__package__.yml @@ -4,6 +4,7 @@ imports: algolia: ../../../algolia.yml rootCommons: ../../../commons.yml apiReadV1: ../../../api/v1/read/__package__.yml + apiReadLatest: ../../../api/latest/__package__.yml apiCommonsV1: ../../../api/v1/commons.yml commons: ../commons/commons.yml navigationV1: ../../../navigation/v1/__package__.yml @@ -67,6 +68,7 @@ types: algoliaSearchIndex: optional pages: map apis: map + apisV2: map files: map filesV2: map jsFiles: diff --git a/packages/commons/fdr-utils/src/definition-object-factory.ts b/packages/commons/fdr-utils/src/definition-object-factory.ts index 5455366756..2d9a86ba4a 100644 --- a/packages/commons/fdr-utils/src/definition-object-factory.ts +++ b/packages/commons/fdr-utils/src/definition-object-factory.ts @@ -5,6 +5,7 @@ export class DefinitionObjectFactory { return { pages: {}, apis: {}, + apisV2: {}, files: {}, filesV2: {}, config: { diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/client/Client.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/client/Client.ts index e9c2fb734a..7462bac3d2 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/api/client/Client.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/client/Client.ts @@ -4,6 +4,7 @@ import * as environments from "../../../../environments"; import * as core from "../../../../core"; +import { Latest } from "../resources/latest/client/Client"; import { V1 } from "../resources/v1/client/Client"; export declare namespace Api { @@ -27,6 +28,12 @@ export declare namespace Api { export class Api { constructor(protected readonly _options: Api.Options = {}) {} + protected _latest: Latest | undefined; + + public get latest(): Latest { + return (this._latest ??= new Latest(this._options)); + } + protected _v1: V1 | undefined; public get v1(): V1 { diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/Client.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/Client.ts new file mode 100644 index 0000000000..e1c2bc70d3 --- /dev/null +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/Client.ts @@ -0,0 +1,92 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as environments from "../../../../../../environments"; +import * as core from "../../../../../../core"; +import * as FernRegistry from "../../../../../index"; +import urlJoin from "url-join"; + +export declare namespace Latest { + interface Options { + environment?: core.Supplier; + token?: core.Supplier; + } + + interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Additional headers to include in the request. */ + headers?: Record; + } +} + +export class Latest { + constructor(protected readonly _options: Latest.Options = {}) {} + + /** + * @param {FernRegistry.ApiDefinitionId} apiDefinitionId + * @param {Latest.RequestOptions} requestOptions - Request-specific configuration. + * + * @example + * await client.api.latest.getApiLatest(FernRegistry.ApiDefinitionId("d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32")) + */ + public async getApiLatest( + apiDefinitionId: FernRegistry.ApiDefinitionId, + requestOptions?: Latest.RequestOptions + ): Promise> { + const _response = await core.fetcher({ + url: urlJoin( + (await core.Supplier.get(this._options.environment)) ?? environments.FernRegistryEnvironment.Prod, + `/registry/api/latest/load/${encodeURIComponent(apiDefinitionId)}` + ), + method: "GET", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-Runtime": core.RUNTIME.type, + "X-Fern-Runtime-Version": core.RUNTIME.version, + ...requestOptions?.headers, + }, + contentType: "application/json", + requestType: "json", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : undefined, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + if (_response.ok) { + return { + ok: true, + body: _response.body as FernRegistry.api.latest.ApiDefinition, + }; + } + + if (_response.error.reason === "status-code") { + switch ((_response.error.body as FernRegistry.api.latest.getApiLatest.Error)?.error) { + case "ApiDoesNotExistError": + return { + ok: false, + error: _response.error.body as FernRegistry.api.latest.getApiLatest.Error, + }; + } + } + + return { + ok: false, + error: FernRegistry.api.latest.getApiLatest.Error._unknown(_response.error), + }; + } + + protected async _getAuthorizationHeader(): Promise { + const bearer = await core.Supplier.get(this._options.token); + if (bearer != null) { + return `Bearer ${bearer}`; + } + + return undefined; + } +} diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/getApiLatest.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/getApiLatest.ts new file mode 100644 index 0000000000..30f9d9445a --- /dev/null +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/getApiLatest.ts @@ -0,0 +1,53 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernRegistry from "../../../../../index"; +import * as core from "../../../../../../core"; + +export type Error = + | FernRegistry.api.latest.getApiLatest.Error.ApiDoesNotExistError + | FernRegistry.api.latest.getApiLatest.Error._Unknown; + +export declare namespace Error { + interface ApiDoesNotExistError { + error: "ApiDoesNotExistError"; + } + + interface _Unknown { + error: void; + content: core.Fetcher.Error; + } + + interface _Visitor<_Result> { + apiDoesNotExistError: () => _Result; + _other: (value: core.Fetcher.Error) => _Result; + } +} + +export const Error = { + apiDoesNotExistError: (): FernRegistry.api.latest.getApiLatest.Error.ApiDoesNotExistError => { + return { + error: "ApiDoesNotExistError", + }; + }, + + _unknown: (fetcherError: core.Fetcher.Error): FernRegistry.api.latest.getApiLatest.Error._Unknown => { + return { + error: undefined, + content: fetcherError, + }; + }, + + _visit: <_Result>( + value: FernRegistry.api.latest.getApiLatest.Error, + visitor: FernRegistry.api.latest.getApiLatest.Error._Visitor<_Result> + ): _Result => { + switch (value.error) { + case "ApiDoesNotExistError": + return visitor.apiDoesNotExistError(); + default: + return visitor._other(value as any); + } + }, +} as const; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/index.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/index.ts new file mode 100644 index 0000000000..50dc679921 --- /dev/null +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/client/index.ts @@ -0,0 +1 @@ +export * as getApiLatest from "./getApiLatest"; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/index.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/index.ts index 3ce0a3e38e..a931b36375 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/index.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/index.ts @@ -1,2 +1,3 @@ export * from "./types"; export * from "./resources"; +export * from "./client"; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/Client.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/Client.ts index 7752d24312..ef37b9dd33 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/Client.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/Client.ts @@ -533,6 +533,206 @@ export class Register { * }] * } * }, + * definitionV2: { + * id: FernRegistry.ApiDefinitionId("d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32"), + * endpoints: { + * "string": { + * id: FernRegistry.EndpointId("string"), + * method: "GET", + * path: [{ + * type: "literal", + * value: { + * "key": "value" + * } + * }], + * auth: [{ + * "key": "value" + * }], + * defaultEnvironment: FernRegistry.EnvironmentId("string"), + * environments: [{ + * "key": "value" + * }], + * pathParameters: [{ + * "key": "value" + * }], + * queryParameters: [{ + * "key": "value" + * }], + * requestHeaders: [{ + * "key": "value" + * }], + * responseHeaders: [{ + * "key": "value" + * }], + * request: { + * contentType: { + * "key": "value" + * }, + * body: { + * type: "object" + * }, + * description: { + * "key": "value" + * } + * }, + * response: { + * body: { + * type: "object" + * }, + * statusCode: 1, + * description: { + * "key": "value" + * } + * }, + * errors: [{ + * "key": "value" + * }], + * examples: [{ + * "key": "value" + * }], + * snippetTemplates: { + * typescript: { + * "key": "value" + * }, + * python: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable", + * namespace: [{ + * "key": "value" + * }] + * } + * }, + * websockets: { + * "string": { + * id: FernRegistry.WebSocketId("string"), + * path: [{ + * type: "literal", + * value: { + * "key": "value" + * } + * }], + * messages: [{ + * type: FernRegistry.api.v1.WebSocketMessageId("string"), + * displayName: { + * "key": "value" + * }, + * origin: "client", + * body: { + * type: "alias", + * value: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable" + * }], + * auth: [{ + * "key": "value" + * }], + * defaultEnvironment: FernRegistry.EnvironmentId("string"), + * environments: [{ + * "key": "value" + * }], + * pathParameters: [{ + * "key": "value" + * }], + * queryParameters: [{ + * "key": "value" + * }], + * requestHeaders: [{ + * "key": "value" + * }], + * examples: [{ + * "key": "value" + * }], + * description: { + * "key": "value" + * }, + * availability: "Stable", + * namespace: [{ + * "key": "value" + * }] + * } + * }, + * webhooks: { + * "string": { + * id: FernRegistry.WebhookId("string"), + * method: "GET", + * path: ["string"], + * headers: [{ + * "key": "value" + * }], + * payload: { + * shape: { + * type: "alias", + * value: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * } + * }, + * examples: [{ + * "key": "value" + * }], + * description: { + * "key": "value" + * }, + * availability: "Stable", + * namespace: [{ + * "key": "value" + * }] + * } + * }, + * types: { + * "string": { + * name: "string", + * shape: { + * type: "alias", + * value: { + * type: "id" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable" + * } + * }, + * subpackages: { + * "string": { + * id: FernRegistry.api.v1.SubpackageId("string"), + * name: "string", + * displayName: "string" + * } + * }, + * auths: { + * "string": { + * type: "bearerAuth" + * } + * }, + * globalHeaders: [{ + * key: FernRegistry.PropertyKey("string"), + * valueShape: { + * type: "alias", + * value: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable" + * }] + * }, * sources: { * "string": { * type: "openapi" diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/requests/RegisterApiDefinitionRequest.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/requests/RegisterApiDefinitionRequest.ts index 835d3ab1c0..eb7c85b09a 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/requests/RegisterApiDefinitionRequest.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/v1/resources/register/client/requests/RegisterApiDefinitionRequest.ts @@ -506,6 +506,206 @@ import * as FernRegistry from "../../../../../../../../index"; * }] * } * }, + * definitionV2: { + * id: FernRegistry.ApiDefinitionId("d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32"), + * endpoints: { + * "string": { + * id: FernRegistry.EndpointId("string"), + * method: "GET", + * path: [{ + * type: "literal", + * value: { + * "key": "value" + * } + * }], + * auth: [{ + * "key": "value" + * }], + * defaultEnvironment: FernRegistry.EnvironmentId("string"), + * environments: [{ + * "key": "value" + * }], + * pathParameters: [{ + * "key": "value" + * }], + * queryParameters: [{ + * "key": "value" + * }], + * requestHeaders: [{ + * "key": "value" + * }], + * responseHeaders: [{ + * "key": "value" + * }], + * request: { + * contentType: { + * "key": "value" + * }, + * body: { + * type: "object" + * }, + * description: { + * "key": "value" + * } + * }, + * response: { + * body: { + * type: "object" + * }, + * statusCode: 1, + * description: { + * "key": "value" + * } + * }, + * errors: [{ + * "key": "value" + * }], + * examples: [{ + * "key": "value" + * }], + * snippetTemplates: { + * typescript: { + * "key": "value" + * }, + * python: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable", + * namespace: [{ + * "key": "value" + * }] + * } + * }, + * websockets: { + * "string": { + * id: FernRegistry.WebSocketId("string"), + * path: [{ + * type: "literal", + * value: { + * "key": "value" + * } + * }], + * messages: [{ + * type: FernRegistry.api.v1.WebSocketMessageId("string"), + * displayName: { + * "key": "value" + * }, + * origin: "client", + * body: { + * type: "alias", + * value: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable" + * }], + * auth: [{ + * "key": "value" + * }], + * defaultEnvironment: FernRegistry.EnvironmentId("string"), + * environments: [{ + * "key": "value" + * }], + * pathParameters: [{ + * "key": "value" + * }], + * queryParameters: [{ + * "key": "value" + * }], + * requestHeaders: [{ + * "key": "value" + * }], + * examples: [{ + * "key": "value" + * }], + * description: { + * "key": "value" + * }, + * availability: "Stable", + * namespace: [{ + * "key": "value" + * }] + * } + * }, + * webhooks: { + * "string": { + * id: FernRegistry.WebhookId("string"), + * method: "GET", + * path: ["string"], + * headers: [{ + * "key": "value" + * }], + * payload: { + * shape: { + * type: "alias", + * value: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * } + * }, + * examples: [{ + * "key": "value" + * }], + * description: { + * "key": "value" + * }, + * availability: "Stable", + * namespace: [{ + * "key": "value" + * }] + * } + * }, + * types: { + * "string": { + * name: "string", + * shape: { + * type: "alias", + * value: { + * type: "id" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable" + * } + * }, + * subpackages: { + * "string": { + * id: FernRegistry.api.v1.SubpackageId("string"), + * name: "string", + * displayName: "string" + * } + * }, + * auths: { + * "string": { + * type: "bearerAuth" + * } + * }, + * globalHeaders: [{ + * key: FernRegistry.PropertyKey("string"), + * valueShape: { + * type: "alias", + * value: { + * "key": "value" + * } + * }, + * description: { + * "key": "value" + * }, + * availability: "Stable" + * }] + * }, * sources: { * "string": { * type: "openapi" @@ -516,6 +716,7 @@ import * as FernRegistry from "../../../../../../../../index"; export interface RegisterApiDefinitionRequest { orgId: FernRegistry.OrgId; apiId: FernRegistry.ApiId; - definition: FernRegistry.api.v1.register.ApiDefinition; + definition?: FernRegistry.api.v1.register.ApiDefinition; + definitionV2?: FernRegistry.api.latest.ApiDefinition; sources?: Record; } diff --git a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts index 3bb18c01c9..9656cbf380 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts @@ -8,6 +8,7 @@ export interface DocsDefinition { algoliaSearchIndex: FernRegistry.AlgoliaSearchIndex | undefined; pages: Record; apis: Record; + apisV2: Record; files: Record; filesV2: Record; /** diff --git a/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts b/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts index b1cc727676..bdee80a71d 100644 --- a/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts +++ b/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts @@ -9,6 +9,7 @@ export function convertDocsDefinitionToRead({ algoliaSearchIndex, filesV2, apis, + apisV2, id, search, }: { @@ -16,6 +17,7 @@ export function convertDocsDefinitionToRead({ algoliaSearchIndex: FernRegistry.AlgoliaSearchIndex | undefined; filesV2: Record; apis: Record; + apisV2: Record; id: APIV1Db.DocsConfigId | undefined; search: SearchInfo; }): DocsV1Read.DocsDefinition { @@ -23,6 +25,7 @@ export function convertDocsDefinitionToRead({ algoliaSearchIndex, pages: docsDbDefinition.pages, apis, + apisV2, files: mapValues(filesV2, (fileV2) => fileV2.url), filesV2, jsFiles: docsDbDefinition.type === "v3" ? docsDbDefinition.jsFiles : undefined, diff --git a/packages/fdr-sdk/src/navigation/utils/toApis.ts b/packages/fdr-sdk/src/navigation/utils/toApis.ts index be70309e8e..e40946d72a 100644 --- a/packages/fdr-sdk/src/navigation/utils/toApis.ts +++ b/packages/fdr-sdk/src/navigation/utils/toApis.ts @@ -3,11 +3,14 @@ import { ApiDefinition } from "../.."; import { DocsV2Read } from "../../client"; export function toApis(docs: DocsV2Read.LoadDocsForUrlResponse) { - return mapValues(docs.definition.apis, (api) => - ApiDefinition.ApiDefinitionV1ToLatest.from(api, { - useJavaScriptAsTypeScript: false, - alwaysEnableJavaScriptFetch: false, - usesApplicationJsonInFormDataValue: false, - }).migrate(), - ); + return { + ...mapValues(docs.definition.apis, (api) => + ApiDefinition.ApiDefinitionV1ToLatest.from(api, { + useJavaScriptAsTypeScript: false, + alwaysEnableJavaScriptFetch: false, + usesApplicationJsonInFormDataValue: false, + }).migrate(), + ), + ...docs.definition.apisV2, + }; } diff --git a/packages/parsers/.gitignore b/packages/parsers/.gitignore index fce751057c..ee8d645970 100644 --- a/packages/parsers/.gitignore +++ b/packages/parsers/.gitignore @@ -1,26 +1,4 @@ -openapi/shared/temporary src/client/generated/Client.ts -src/client/generated/api/resources/api/client/ -src/client/generated/api/resources/api/resources/v1/client/ -src/client/generated/api/resources/api/resources/v1/resources/read/client/ -src/client/generated/api/resources/api/resources/v1/resources/register/client/ -src/client/generated/api/resources/diff/client/ -src/client/generated/api/resources/docs/client/ -src/client/generated/api/resources/docs/resources/v1/client/ -src/client/generated/api/resources/docs/resources/v1/resources/read/client/ -src/client/generated/api/resources/docs/resources/v1/resources/write/client/ -src/client/generated/api/resources/docs/resources/v2/client/ -src/client/generated/api/resources/docs/resources/v2/resources/read/client/ -src/client/generated/api/resources/docs/resources/v2/resources/write/client/ +src/client/generated/api/resources/**/client/ src/client/generated/api/resources/docsCache/ -src/client/generated/api/resources/generators/client/ -src/client/generated/api/resources/generators/resources/cli/client/ -src/client/generated/api/resources/generators/resources/versions/client/ -src/client/generated/api/resources/git/client/ -src/client/generated/api/resources/sdks/client/ -src/client/generated/api/resources/sdks/resources/versions/client/ -src/client/generated/api/resources/snippets/client/ -src/client/generated/api/resources/snippetsFactory/client/ -src/client/generated/api/resources/templates/client/ -src/client/generated/api/resources/tokens/client/ src/client/generated/core/ \ No newline at end of file diff --git a/packages/parsers/src/client/generated/.fernignore b/packages/parsers/src/client/generated/.fernignore index 3cf60e09c4..903cbd4e71 100644 --- a/packages/parsers/src/client/generated/.fernignore +++ b/packages/parsers/src/client/generated/.fernignore @@ -3,6 +3,8 @@ Client.ts index.ts api/resources/api/client/ api/resources/api/index.ts +api/resources/api/resources/latest/client/ +api/resources/api/resources/latest/index.ts api/resources/api/resources/v1/client/ api/resources/api/resources/v1/index.ts api/resources/api/resources/v1/resources/read/client/ diff --git a/packages/parsers/src/client/generated/api/resources/api/resources/latest/index.ts b/packages/parsers/src/client/generated/api/resources/api/resources/latest/index.ts index 3ce0a3e38e..3e15e29078 100644 --- a/packages/parsers/src/client/generated/api/resources/api/resources/latest/index.ts +++ b/packages/parsers/src/client/generated/api/resources/api/resources/latest/index.ts @@ -1,2 +1,2 @@ -export * from "./types"; export * from "./resources"; +export * from "./types"; diff --git a/packages/parsers/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts b/packages/parsers/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts index 3bb18c01c9..9656cbf380 100644 --- a/packages/parsers/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts +++ b/packages/parsers/src/client/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.ts @@ -8,6 +8,7 @@ export interface DocsDefinition { algoliaSearchIndex: FernRegistry.AlgoliaSearchIndex | undefined; pages: Record; apis: Record; + apisV2: Record; files: Record; filesV2: Record; /** diff --git a/packages/parsers/tsconfig.json b/packages/parsers/tsconfig.json index 4963f81acc..04fc8db911 100644 --- a/packages/parsers/tsconfig.json +++ b/packages/parsers/tsconfig.json @@ -11,29 +11,8 @@ "exclude": [ "node_modules", "src/client/generated/Client.ts", - "src/client/generated/api/resources/api/client/", - "src/client/generated/api/resources/api/resources/v1/client/", - "src/client/generated/api/resources/api/resources/v1/resources/read/client/", - "src/client/generated/api/resources/api/resources/v1/resources/register/client/", - "src/client/generated/api/resources/diff/client/", - "src/client/generated/api/resources/docs/client/", - "src/client/generated/api/resources/docs/resources/v1/client/", - "src/client/generated/api/resources/docs/resources/v1/resources/read/client/", - "src/client/generated/api/resources/docs/resources/v1/resources/write/client/", - "src/client/generated/api/resources/docs/resources/v2/client/", - "src/client/generated/api/resources/docs/resources/v2/resources/read/client/", - "src/client/generated/api/resources/docs/resources/v2/resources/write/client/", + "src/client/generated/api/resources/**/client/", "src/client/generated/api/resources/docsCache/", - "src/client/generated/api/resources/generators/client/", - "src/client/generated/api/resources/generators/resources/cli/client/", - "src/client/generated/api/resources/generators/resources/versions/client/", - "src/client/generated/api/resources/git/client/", - "src/client/generated/api/resources/sdks/client/", - "src/client/generated/api/resources/sdks/resources/versions/client/", - "src/client/generated/api/resources/snippets/client/", - "src/client/generated/api/resources/snippetsFactory/client/", - "src/client/generated/api/resources/templates/client/", - "src/client/generated/api/resources/tokens/client/", "src/client/generated/core/" ], "references": [] diff --git a/packages/ui/app/src/resolver/resolveDocsContent.ts b/packages/ui/app/src/resolver/resolveDocsContent.ts index 7a031f6643..b1a1a8867c 100644 --- a/packages/ui/app/src/resolver/resolveDocsContent.ts +++ b/packages/ui/app/src/resolver/resolveDocsContent.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { ApiDefinitionV1ToLatest } from "@fern-api/fdr-sdk/api-definition"; -import type { APIV1Read, DocsV1Read } from "@fern-api/fdr-sdk/client/types"; +import type { APIV1Read, DocsV1Read, FdrAPI } from "@fern-api/fdr-sdk/client/types"; import * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { ApiDefinitionLoader, MarkdownLoader } from "@fern-ui/fern-docs-server"; import type { FeatureFlags } from "@fern-ui/fern-docs-utils"; @@ -28,6 +28,7 @@ interface ResolveDocsContentArgs { prev: FernNavigation.NavigationNodeNeighbor | undefined; next: FernNavigation.NavigationNodeNeighbor | undefined; apis: Record; + apisV2: Record; pages: Record; mdxOptions?: FernSerializeMdxOptions; featureFlags: FeatureFlags; @@ -45,6 +46,7 @@ export async function resolveDocsContent({ prev, next, apis, + apisV2, pages, mdxOptions, featureFlags, @@ -64,14 +66,25 @@ export async function resolveDocsContent({ engine, ); - const apiLoaders = mapValues(apis, (api) => { - return ApiDefinitionLoader.create(domain, api.id) - .withMdxBundler(serializeMdx, engine) - .withFlags(featureFlags) - .withApiDefinition(ApiDefinitionV1ToLatest.from(api, featureFlags).migrate()) - .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN) - .withResolveDescriptions(); - }); + // TODO: remove legacy when done + const apiLoaders = { + ...mapValues(apis, (api) => { + return ApiDefinitionLoader.create(domain, api.id) + .withMdxBundler(serializeMdx, engine) + .withFlags(featureFlags) + .withApiDefinition(ApiDefinitionV1ToLatest.from(api, featureFlags).migrate()) + .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN) + .withResolveDescriptions(); + }), + ...mapValues(apisV2 ?? {}, (api) => { + return ApiDefinitionLoader.create(domain, api.id) + .withMdxBundler(serializeMdx, engine) + .withFlags(featureFlags) + .withApiDefinition(api) + .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN) + .withResolveDescriptions(); + }), + }; let result: DocsContent | undefined; diff --git a/packages/ui/docs-bundle/src/server/DocsLoader.ts b/packages/ui/docs-bundle/src/server/DocsLoader.ts index 37abe709f8..6e344d0756 100644 --- a/packages/ui/docs-bundle/src/server/DocsLoader.ts +++ b/packages/ui/docs-bundle/src/server/DocsLoader.ts @@ -87,10 +87,12 @@ export class DocsLoader { return undefined; } const v1 = res.definition.apis[key]; - if (!v1) { + const latest = + res.definition.apisV2?.[key] ?? + (v1 != null ? ApiDefinitionV1ToLatest.from(v1, this.featureFlags).migrate() : undefined); + if (!latest) { return undefined; } - const latest = ApiDefinitionV1ToLatest.from(v1, this.featureFlags).migrate(); return ApiDefinitionLoader.create(this.domain, key) .withApiDefinition(latest) .withFlags(this.featureFlags) diff --git a/packages/ui/docs-bundle/src/server/withResolvedDocsContent.ts b/packages/ui/docs-bundle/src/server/withResolvedDocsContent.ts index bd65eee3d3..0034d98904 100644 --- a/packages/ui/docs-bundle/src/server/withResolvedDocsContent.ts +++ b/packages/ui/docs-bundle/src/server/withResolvedDocsContent.ts @@ -54,6 +54,7 @@ export async function withResolvedDocsContent({ next: featureFlags.isAuthenticatedPagesDiscoverable ? found.next : found.next?.authed ? undefined : found.next, apis: definition.apis, + apisV2: definition.apisV2, pages: definition.pages, featureFlags, mdxOptions: { diff --git a/packages/ui/fern-docs-search-server/src/algolia/__test__/test-utils.ts b/packages/ui/fern-docs-search-server/src/algolia/__test__/test-utils.ts index 78b9d18f96..c7cfde6008 100644 --- a/packages/ui/fern-docs-search-server/src/algolia/__test__/test-utils.ts +++ b/packages/ui/fern-docs-search-server/src/algolia/__test__/test-utils.ts @@ -22,18 +22,21 @@ export function readFixtureToRootNode(fixture: DocsV2Read.LoadDocsForUrlResponse pages: Record; } { const root = FernNavigation.utils.toRootNode(fixture); - const apis = Object.fromEntries( - Object.values(fixture.definition.apis).map((api) => { - return [ - api.id, - ApiDefinition.ApiDefinitionV1ToLatest.from(api, { - useJavaScriptAsTypeScript: false, - alwaysEnableJavaScriptFetch: false, - usesApplicationJsonInFormDataValue: false, - }).migrate(), - ]; - }), - ); + const apis = { + ...Object.fromEntries( + Object.values(fixture.definition.apis).map((api) => { + return [ + api.id, + ApiDefinition.ApiDefinitionV1ToLatest.from(api, { + useJavaScriptAsTypeScript: false, + alwaysEnableJavaScriptFetch: false, + usesApplicationJsonInFormDataValue: false, + }).migrate(), + ]; + }), + ), + ...fixture.definition.apisV2, + }; const pages = mapValues(fixture.definition.pages, (page) => page.markdown); return { root, apis, pages }; } diff --git a/packages/ui/fern-docs-search-server/src/fdr/load-docs-with-url.ts b/packages/ui/fern-docs-search-server/src/fdr/load-docs-with-url.ts index 296569a24d..8f30c89431 100644 --- a/packages/ui/fern-docs-search-server/src/fdr/load-docs-with-url.ts +++ b/packages/ui/fern-docs-search-server/src/fdr/load-docs-with-url.ts @@ -63,13 +63,16 @@ export async function loadDocsWithUrl(payload: LoadDocsWithUrlPayload): Promise< const pages = mapValues(docs.body.definition.pages, (page) => page.markdown); // migrate apis - const apis = mapValues(docs.body.definition.apis, (api) => - ApiDefinition.ApiDefinitionV1ToLatest.from(api, { - useJavaScriptAsTypeScript: payload.useJavaScriptAsTypeScript ?? false, - alwaysEnableJavaScriptFetch: payload.alwaysEnableJavaScriptFetch ?? false, - usesApplicationJsonInFormDataValue: payload.usesApplicationJsonInFormDataValue ?? false, - }).migrate(), - ); + const apis = { + ...mapValues(docs.body.definition.apis, (api) => + ApiDefinition.ApiDefinitionV1ToLatest.from(api, { + useJavaScriptAsTypeScript: payload.useJavaScriptAsTypeScript ?? false, + alwaysEnableJavaScriptFetch: payload.alwaysEnableJavaScriptFetch ?? false, + usesApplicationJsonInFormDataValue: payload.usesApplicationJsonInFormDataValue ?? false, + }).migrate(), + ), + ...docs.body.definition.apisV2, + }; return { org_id: org.body, root, pages, apis, domain: domain.host }; } diff --git a/packages/ui/fern-docs-server/src/ApiDefinitionLoader.ts b/packages/ui/fern-docs-server/src/ApiDefinitionLoader.ts index 023ff80474..ad563f354e 100644 --- a/packages/ui/fern-docs-server/src/ApiDefinitionLoader.ts +++ b/packages/ui/fern-docs-server/src/ApiDefinitionLoader.ts @@ -100,18 +100,36 @@ export class ApiDefinitionLoader { // return apiDefinition; // } + const latest = await this.#getClient().api.latest.getApiLatest(this.apiDefinitionId); + if (!latest.ok) { + if (latest.error.error === "ApiDoesNotExistError") { + return undefined; + } else { + // eslint-disable-next-line no-console + console.error(latest?.error?.content); + throw new Error("Failed to load API definition"); + } + } + if (latest.ok) { + return latest.body; + } + const v1 = await this.#getClient().api.v1.read.getApi(this.apiDefinitionId); if (!v1.ok) { if (v1.error.error === "ApiDoesNotExistError") { return undefined; } else { // eslint-disable-next-line no-console - console.error(v1.error.content); + console.error(v1?.error?.content); throw new Error("Failed to load API definition"); } } + if (v1.ok) { + return ApiDefinitionV1ToLatest.from(v1.body, this.flags).migrate(); + } + + return undefined; - return ApiDefinitionV1ToLatest.from(v1.body, this.flags).migrate(); // await this.cache.setApiDefinition(apiDefinition); // return apiDefinition; }; diff --git a/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts b/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts index 16b78da8ed..47e1ead114 100644 --- a/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts +++ b/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts @@ -70,6 +70,7 @@ export async function getDocsPageProps( prev: node.prev, next: node.next, apis: docs.definition.apis, + apisV2: docs.definition.apisV2, pages: docs.definition.pages, featureFlags, mdxOptions: { diff --git a/servers/fdr/prisma/migrations/20241217171736_add_api_definitions_latest/migration.sql b/servers/fdr/prisma/migrations/20241217171736_add_api_definitions_latest/migration.sql new file mode 100644 index 0000000000..4363c84085 --- /dev/null +++ b/servers/fdr/prisma/migrations/20241217171736_add_api_definitions_latest/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "ApiDefinitionsLatest" ( + "orgId" TEXT NOT NULL, + "apiName" TEXT NOT NULL, + "apiDefinitionId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "definition" BYTEA NOT NULL, + + CONSTRAINT "ApiDefinitionsLatest_pkey" PRIMARY KEY ("apiDefinitionId") +); diff --git a/servers/fdr/prisma/schema.prisma b/servers/fdr/prisma/schema.prisma index f0e491eaba..3bdf5afc48 100644 --- a/servers/fdr/prisma/schema.prisma +++ b/servers/fdr/prisma/schema.prisma @@ -21,6 +21,16 @@ model ApiDefinitionsV2 { @@id([apiDefinitionId]) } +model ApiDefinitionsLatest { + orgId String + apiName String + apiDefinitionId String + createdAt DateTime @default(now()) + definition Bytes + + @@id([apiDefinitionId]) +} + model DocsRegistrations { registrationID String orgID String diff --git a/servers/fdr/src/__test__/local/services/api.test.ts b/servers/fdr/src/__test__/local/services/api.test.ts index 853736b7f0..949a650e04 100644 --- a/servers/fdr/src/__test__/local/services/api.test.ts +++ b/servers/fdr/src/__test__/local/services/api.test.ts @@ -1,6 +1,6 @@ import { APIV1Write, FdrAPI } from "@fern-api/fdr-sdk"; import { inject } from "vitest"; -import { createApiDefinition, getAPIResponse, getClient } from "../util"; +import { createApiDefinition, createApiDefinitionLatest, getAPIResponse, getClient } from "../util"; export const EMPTY_REGISTER_API_DEFINITION: APIV1Write.ApiDefinition = { rootPackage: { @@ -72,3 +72,64 @@ it("register api", async () => { JSON.stringify(MOCK_REGISTER_API_DEFINITION.subpackages), ); }); + +const EMPTY_REGISTER_API_LATEST_DEFINITION: FdrAPI.api.latest.ApiDefinition = { + endpoints: {}, + types: {}, + subpackages: {}, + websockets: {}, + webhooks: {}, + id: FdrAPI.ApiDefinitionId("api"), + auths: {}, + globalHeaders: undefined, +}; + +const MOCK_REGISTER_API_LATEST_DEFINITION: FdrAPI.api.latest.ApiDefinition = createApiDefinitionLatest({ + endpointId: FdrAPI.EndpointId("dummy"), + endpointMethod: "POST", + endpointPath: [{ type: "literal" as const, value: "dummy" }], +}); + +it("register api latest", async () => { + const fdr = getClient({ authed: true, url: inject("url") }); + const emptyDefinitionRegisterResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("fern"), + apiId: FdrAPI.ApiId("api"), + definitionV2: EMPTY_REGISTER_API_LATEST_DEFINITION, + }), + ); + + console.log(`Registered empty definition. Received ${emptyDefinitionRegisterResponse.apiDefinitionId}`); + // load empty definition + const registeredEmptyDefinition = getAPIResponse( + await fdr.api.latest.getApiLatest(emptyDefinitionRegisterResponse.apiDefinitionId), + ); + + // assert definitions are equal + expect(JSON.stringify(registeredEmptyDefinition.types)).toEqual( + JSON.stringify(EMPTY_REGISTER_API_LATEST_DEFINITION.types), + ); + expect(JSON.stringify(registeredEmptyDefinition.subpackages)).toEqual( + JSON.stringify(EMPTY_REGISTER_API_LATEST_DEFINITION.subpackages), + ); + expect(registeredEmptyDefinition).toEqual(EMPTY_REGISTER_API_LATEST_DEFINITION); + + // register updated definition + const updatedDefinitionRegisterResponse = getAPIResponse( + await fdr.api.v1.register.registerApiDefinition({ + orgId: FdrAPI.OrgId("fern"), + apiId: FdrAPI.ApiId("api"), + definitionV2: MOCK_REGISTER_API_LATEST_DEFINITION, + }), + ); + // load updated definition + const updatedDefinition = getAPIResponse( + await fdr.api.latest.getApiLatest(updatedDefinitionRegisterResponse.apiDefinitionId), + ); + // assert definitions equal + expect(JSON.stringify(updatedDefinition.types)).toEqual(JSON.stringify(MOCK_REGISTER_API_LATEST_DEFINITION.types)); + expect(JSON.stringify(updatedDefinition.subpackages)).toEqual( + JSON.stringify(MOCK_REGISTER_API_LATEST_DEFINITION.subpackages), + ); +}); diff --git a/servers/fdr/src/__test__/local/setupMockFdr.ts b/servers/fdr/src/__test__/local/setupMockFdr.ts index e09466c3b0..659f820bcc 100644 --- a/servers/fdr/src/__test__/local/setupMockFdr.ts +++ b/servers/fdr/src/__test__/local/setupMockFdr.ts @@ -5,6 +5,7 @@ import express from "express"; import http from "http"; import { register } from "../../api"; import { FdrApplication, FdrConfig } from "../../app"; +import { getApiLatestService } from "../../controllers/api/getApiLatestService"; import { getReadApiService } from "../../controllers/api/getApiReadService"; import { getRegisterApiService } from "../../controllers/api/getRegisterApiService"; import { getApiDiffService } from "../../controllers/diff/getApiDiffService"; @@ -106,6 +107,7 @@ async function runMockFdr(port: number): Promise { read: { _root: getReadApiService(fdrApplication) }, register: { _root: getRegisterApiService(fdrApplication) }, }, + latest: { _root: getApiLatestService(fdrApplication) }, }, snippets: getSnippetsService(fdrApplication), snippetsFactory: getSnippetsFactoryService(fdrApplication), diff --git a/servers/fdr/src/__test__/local/util.ts b/servers/fdr/src/__test__/local/util.ts index 4fda531b02..69e6f4b6df 100644 --- a/servers/fdr/src/__test__/local/util.ts +++ b/servers/fdr/src/__test__/local/util.ts @@ -1,4 +1,4 @@ -import { APIResponse, APIV1Write, FdrClient } from "@fern-api/fdr-sdk"; +import { APIResponse, APIV1Write, FdrAPI, FdrClient } from "@fern-api/fdr-sdk"; import type { DocsV2, IndexSegment } from "@prisma/client"; export function getUniqueDocsForUrl(prefix: string): string { @@ -56,6 +56,48 @@ export function createApiDefinition({ }; } +export function createApiDefinitionLatest({ + endpointId, + endpointPath, + endpointMethod, +}: { + endpointId: FdrAPI.EndpointId; + endpointPath: FdrAPI.api.latest.PathPart[]; + endpointMethod: FdrAPI.HttpMethod; +}): FdrAPI.api.latest.ApiDefinition { + return { + endpoints: { + [FdrAPI.EndpointId(endpointId)]: { + id: FdrAPI.EndpointId(endpointId), + method: endpointMethod, + path: endpointPath, + requestHeaders: undefined, + responseHeaders: undefined, + queryParameters: undefined, + pathParameters: undefined, + snippetTemplates: undefined, + namespace: undefined, + examples: undefined, + auth: undefined, + defaultEnvironment: undefined, + environments: undefined, + request: undefined, + response: undefined, + errors: undefined, + description: undefined, + availability: undefined, + }, + }, + types: {}, + subpackages: {}, + websockets: {}, + webhooks: {}, + id: FdrAPI.ApiDefinitionId("api"), + auths: {}, + globalHeaders: undefined, + }; +} + export function createMockDocs({ domain, path, diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.d.ts b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.d.ts new file mode 100644 index 0000000000..b53ab3f415 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.d.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +import * as errors from "../../../../../../errors/index"; +import express from "express"; +export declare class ApiDoesNotExistError extends errors.FernRegistryError { + constructor(); + send(res: express.Response): Promise; +} diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.js b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.js new file mode 100644 index 0000000000..f8f0357154 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/ApiDoesNotExistError.js @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import * as errors from "../../../../../../errors/index"; +export class ApiDoesNotExistError extends errors.FernRegistryError { + constructor() { + super("ApiDoesNotExistError"); + Object.setPrototypeOf(this, ApiDoesNotExistError.prototype); + } + send(res) { + return __awaiter(this, void 0, void 0, function* () { + res.sendStatus(404); + }); + } +} diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.d.ts b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.d.ts new file mode 100644 index 0000000000..405c4eaa80 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.d.ts @@ -0,0 +1 @@ +export * from "./ApiDoesNotExistError"; diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.js b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.js new file mode 100644 index 0000000000..405c4eaa80 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/errors/index.js @@ -0,0 +1 @@ +export * from "./ApiDoesNotExistError"; diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.d.ts b/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.d.ts index 3ce0a3e38e..8c7c513b6d 100644 --- a/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.d.ts +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.d.ts @@ -1,2 +1,4 @@ export * from "./types"; export * from "./resources"; +export * from "./service"; +export * from "./errors"; diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.js b/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.js index 3ce0a3e38e..8c7c513b6d 100644 --- a/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.js +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/index.js @@ -1,2 +1,4 @@ export * from "./types"; export * from "./resources"; +export * from "./service"; +export * from "./errors"; diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.d.ts b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.d.ts new file mode 100644 index 0000000000..734c64f517 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.d.ts @@ -0,0 +1,21 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +import * as FernRegistry from "../../../../../index"; +import express from "express"; +export interface LatestServiceMethods { + getApiLatest(req: express.Request<{ + apiDefinitionId: FernRegistry.ApiDefinitionId; + }, FernRegistry.api.latest.ApiDefinition, never, never>, res: { + send: (responseBody: FernRegistry.api.latest.ApiDefinition) => Promise; + cookie: (cookie: string, value: string, options?: express.CookieOptions) => void; + locals: any; + }, next: express.NextFunction): void | Promise; +} +export declare class LatestService { + private readonly methods; + private router; + constructor(methods: LatestServiceMethods, middleware?: express.RequestHandler[]); + addMiddleware(handler: express.RequestHandler): this; + toRouter(): express.Router; +} diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.js b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.js new file mode 100644 index 0000000000..7e4809b68d --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/LatestService.js @@ -0,0 +1,58 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import express from "express"; +import * as errors from "../../../../../../errors/index"; +export class LatestService { + constructor(methods, middleware = []) { + this.methods = methods; + this.router = express.Router({ mergeParams: true }).use(express.json({ + strict: false, + }), ...middleware); + } + addMiddleware(handler) { + this.router.use(handler); + return this; + } + toRouter() { + this.router.get("/load/:apiDefinitionId", (req, res, next) => __awaiter(this, void 0, void 0, function* () { + try { + yield this.methods.getApiLatest(req, { + send: (responseBody) => __awaiter(this, void 0, void 0, function* () { + res.json(responseBody); + }), + cookie: res.cookie.bind(res), + locals: res.locals, + }, next); + next(); + } + catch (error) { + if (error instanceof errors.FernRegistryError) { + switch (error.errorName) { + case "ApiDoesNotExistError": + break; + default: + console.warn(`Endpoint 'getApiLatest' unexpectedly threw ${error.constructor.name}.` + + ` If this was intentional, please add ${error.constructor.name} to` + + " the endpoint's errors list in your Fern Definition."); + } + yield error.send(res); + } + else { + res.status(500).json("Internal Server Error"); + } + next(error); + } + })); + return this.router; + } +} diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.d.ts b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.d.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.js b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.js new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/api/resources/latest/service/index.js @@ -0,0 +1 @@ +export {}; diff --git a/servers/fdr/src/api/generated/api/resources/api/resources/v1/resources/register/service/requests/RegisterApiDefinitionRequest.d.ts b/servers/fdr/src/api/generated/api/resources/api/resources/v1/resources/register/service/requests/RegisterApiDefinitionRequest.d.ts index 1d6ff6880b..84c56b3b2e 100644 --- a/servers/fdr/src/api/generated/api/resources/api/resources/v1/resources/register/service/requests/RegisterApiDefinitionRequest.d.ts +++ b/servers/fdr/src/api/generated/api/resources/api/resources/v1/resources/register/service/requests/RegisterApiDefinitionRequest.d.ts @@ -5,6 +5,7 @@ import * as FernRegistry from "../../../../../../../../index"; export interface RegisterApiDefinitionRequest { orgId: FernRegistry.OrgId; apiId: FernRegistry.ApiId; - definition: FernRegistry.api.v1.register.ApiDefinition; + definition?: FernRegistry.api.v1.register.ApiDefinition; + definitionV2?: FernRegistry.api.latest.ApiDefinition; sources?: Record; } diff --git a/servers/fdr/src/api/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.d.ts b/servers/fdr/src/api/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.d.ts index 5602817623..1dbb2d746d 100644 --- a/servers/fdr/src/api/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.d.ts +++ b/servers/fdr/src/api/generated/api/resources/docs/resources/v1/resources/read/types/DocsDefinition.d.ts @@ -6,6 +6,7 @@ export interface DocsDefinition { algoliaSearchIndex: FernRegistry.AlgoliaSearchIndex | undefined; pages: Record; apis: Record; + apisV2: Record; files: Record; filesV2: Record; /** diff --git a/servers/fdr/src/api/generated/register.d.ts b/servers/fdr/src/api/generated/register.d.ts index c9a2597a61..783ba18a28 100644 --- a/servers/fdr/src/api/generated/register.d.ts +++ b/servers/fdr/src/api/generated/register.d.ts @@ -9,6 +9,7 @@ import { SnippetsFactoryService } from "./api/resources/snippetsFactory/service/ import { SnippetsService } from "./api/resources/snippets/service/SnippetsService"; import { TemplatesService } from "./api/resources/templates/service/TemplatesService"; import { TokensService } from "./api/resources/tokens/service/TokensService"; +import { LatestService as api_latest_RootService } from "./api/resources/api/resources/latest/service/LatestService"; import { ReadService as api_v1_read_RootService } from "./api/resources/api/resources/v1/resources/read/service/ReadService"; import { RegisterService as api_v1_register_RootService } from "./api/resources/api/resources/v1/resources/register/service/RegisterService"; import { ReadService as docs_v1_read_RootService } from "./api/resources/docs/resources/v1/resources/read/service/ReadService"; @@ -28,6 +29,9 @@ export declare function register(expressApp: express.Express | express.Router, s templates: TemplatesService; tokens: TokensService; api: { + latest: { + _root: api_latest_RootService; + }; v1: { read: { _root: api_v1_read_RootService; diff --git a/servers/fdr/src/api/generated/register.js b/servers/fdr/src/api/generated/register.js index 3e624c2fb5..a114132e1c 100644 --- a/servers/fdr/src/api/generated/register.js +++ b/servers/fdr/src/api/generated/register.js @@ -2,6 +2,7 @@ * This file was auto-generated by Fern from our API Definition. */ export function register(expressApp, services) { + expressApp.use("/registry/api/latest", services.api.latest._root.toRouter()); expressApp.use("/registry/api", services.api.v1.read._root.toRouter()); expressApp.use("/registry/api", services.api.v1.register._root.toRouter()); expressApp.use("/registry/docs", services.docs.v1.read._root.toRouter()); diff --git a/servers/fdr/src/api/index.ts b/servers/fdr/src/api/index.ts index fe240b9774..43e36a183d 100644 --- a/servers/fdr/src/api/index.ts +++ b/servers/fdr/src/api/index.ts @@ -1,4 +1,5 @@ export * as Algolia from "./generated/api/resources/algolia"; +export { LatestService as APILatestService } from "./generated/api/resources/api/resources/latest/service/LatestService"; export { ReadService as APIV1ReadService } from "./generated/api/resources/api/resources/v1/resources/read/service/ReadService"; export { RegisterService as APIV1WriteService } from "./generated/api/resources/api/resources/v1/resources/register/service/RegisterService"; export { DiffService } from "./generated/api/resources/diff/service/DiffService"; diff --git a/servers/fdr/src/controllers/api/getApiLatestService.ts b/servers/fdr/src/controllers/api/getApiLatestService.ts new file mode 100644 index 0000000000..82fbd4baeb --- /dev/null +++ b/servers/fdr/src/controllers/api/getApiLatestService.ts @@ -0,0 +1,35 @@ +import { APILatestService } from "../../api"; +import { UserNotInOrgError } from "../../api/generated/api"; +import { ApiDoesNotExistError } from "../../api/generated/api/resources/api/resources/v1/resources/read/errors"; +import type { FdrApplication } from "../../app"; + +export function getApiLatestService(app: FdrApplication): APILatestService { + return new APILatestService({ + getApiLatest: 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 apiDefinition = await app.dao.apis().loadAPILatestDefinition(req.params.apiDefinitionId); + if (apiDefinition == null) { + throw new ApiDoesNotExistError(); + } + return res.send(apiDefinition); + }, + }); +} diff --git a/servers/fdr/src/controllers/api/getRegisterApiService.ts b/servers/fdr/src/controllers/api/getRegisterApiService.ts index 650cf6c03d..74c50cecda 100644 --- a/servers/fdr/src/controllers/api/getRegisterApiService.ts +++ b/servers/fdr/src/controllers/api/getRegisterApiService.ts @@ -1,4 +1,4 @@ -import { APIV1Write, FdrAPI, SDKSnippetHolder, convertAPIDefinitionToDb } from "@fern-api/fdr-sdk"; +import { APIV1Db, 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"; @@ -21,63 +21,85 @@ export function getRegisterApiService(app: FdrApplication): APIV1WriteService { 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 apiDefinitionId = FdrAPI.ApiDefinitionId(uuidv4()); + let transformedApiDefinition: APIV1Db.DbApiDefinition | FdrAPI.api.latest.ApiDefinition | undefined; + + console.log(req.body.definition); + if (req.body.definition != null && Object.keys(req.body.definition).length > 0) { + 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 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 snippetHolder = new SDKSnippetHolder({ + snippetsBySdkId, + snippetsBySdkIdAndEndpointId, + snippetsConfigWithSdkId: snippetsConfigurationWithSdkIds, + snippetTemplatesByEndpoint, + snippetTemplatesByEndpointId, + }); + 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 isLatest = false; + if (transformedApiDefinition == null) { + if ( + req.body.definitionV2 == null || + (req.body.definitionV2 != null && Object.keys(req.body.definitionV2).length === 0) + ) { + throw new Error("No latest definition provided"); + } + transformedApiDefinition = req.body.definitionV2; + isLatest = true; + } let sources: Record | undefined; if (req.body.sources != null) { @@ -98,7 +120,9 @@ export function getRegisterApiService(app: FdrApplication): APIV1WriteService { `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({ + await ( + isLatest ? app.services.db.prisma.apiDefinitionsLatest : app.services.db.prisma.apiDefinitionsV2 + ).create({ data: { apiDefinitionId, apiName: req.body.apiId, diff --git a/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts b/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts index 93cbeabb65..28fe11547c 100644 --- a/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts +++ b/servers/fdr/src/controllers/docs/v1/getDocsReadService.ts @@ -115,7 +115,7 @@ export async function getDocsDefinition({ docsDbDefinition: DocsV1Db.DocsDefinitionDb; docsV2: LoadDocsDefinitionByUrlResponse | null; }): Promise { - const [apiDefinitions, searchInfo] = await Promise.all([ + const [apiDefinitions, apiV2Definitions, searchInfo] = await Promise.all([ app.services.db.prisma.apiDefinitionsV2.findMany({ where: { apiDefinitionId: { @@ -123,6 +123,13 @@ export async function getDocsDefinition({ }, }, }), + app.services.db.prisma.apiDefinitionsLatest.findMany({ + where: { + apiDefinitionId: { + in: Array.from(docsDbDefinition.referencedApis), + }, + }, + }), loadIndexSegmentsAndGetSearchInfo({ app, docsDbDefinition, @@ -138,11 +145,17 @@ export async function getDocsDefinition({ convertDbApiDefinitionToRead(def.definition), ); + const apiV2DefinitionsById = mapValues( + keyBy(apiV2Definitions, (def) => FernNavigation.ApiDefinitionId(def.apiDefinitionId)), + (def) => readBuffer(def.definition) as FdrAPI.api.latest.ApiDefinition, + ); + return convertDocsDefinitionToRead({ docsDbDefinition, algoliaSearchIndex: docsV2?.algoliaIndex ?? undefined, filesV2, apis: apiDefinitionsById, + apisV2: apiV2DefinitionsById, id: docsV2?.docsConfigInstanceId ?? undefined, search: searchInfo, }); diff --git a/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts b/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts index 57d407aa56..adf1cc79fe 100644 --- a/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts +++ b/servers/fdr/src/controllers/docs/v2/getDocsWriteV2Service.ts @@ -186,9 +186,20 @@ export function getDocsWriteV2Service(app: FdrApplication): DocsV2WriteService { dbDocsDefinition.referencedApis.map(async (id) => await app.services.db.getApiDefinition(id)), ) ).filter(isNonNullish); + const apiDefinitionsLatest = ( + await Promise.all( + dbDocsDefinition.referencedApis.map( + async (id) => await app.services.db.getApiLatestDefinition(id), + ), + ) + ).filter(isNonNullish); + const apiDefinitionsById = Object.fromEntries( apiDefinitions.map((definition) => [definition.id, definition]), ); + const apiDefinitionsLatestById = Object.fromEntries( + apiDefinitionsLatest.map((definition) => [definition.id, definition]), + ); const warmEndpointCachePromises = apiDefinitions.flatMap((apiDefinition) => { return Object.entries(apiDefinition.subpackages).flatMap(([id, subpackage]) => { @@ -212,6 +223,7 @@ export function getDocsWriteV2Service(app: FdrApplication): DocsV2WriteService { docsRegistrationInfo, dbDocsDefinition, apiDefinitionsById, + apiDefinitionsLatestById, ); await app.docsDefinitionCache.storeDocsForUrl({ @@ -285,12 +297,24 @@ export function getDocsWriteV2Service(app: FdrApplication): DocsV2WriteService { apiDefinitions.map((definition) => [definition.id, definition]), ); + const apiDefinitionsLatest = ( + await Promise.all( + response.docsDefinition.referencedApis.map( + async (id) => await app.services.db.getApiLatestDefinition(id), + ), + ) + ).filter(isNonNullish); + const apiDefinitionsLatestById = Object.fromEntries( + apiDefinitionsLatest.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, + apiDefinitionsLatestById, response.algoliaIndex, response.docsConfigInstanceId, ); @@ -328,6 +352,7 @@ async function uploadToAlgoliaForRegistration( docsRegistrationInfo: DocsRegistrationInfo, dbDocsDefinition: WithoutQuestionMarks, apiDefinitionsById: Record, + apiDefinitionsLatestById: 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/ @@ -340,7 +365,13 @@ async function uploadToAlgoliaForRegistration( return []; } - return uploadToAlgolia(app, docsRegistrationInfo.fernUrl, dbDocsDefinition, apiDefinitionsById); + return uploadToAlgolia( + app, + docsRegistrationInfo.fernUrl, + dbDocsDefinition, + apiDefinitionsById, + apiDefinitionsLatestById, + ); } async function uploadToAlgolia( @@ -348,6 +379,7 @@ async function uploadToAlgolia( url: ParsedBaseUrl, dbDocsDefinition: WithoutQuestionMarks, apiDefinitionsById: Record, + apiDefinitionsLatestById: Record, algoliaIndex?: FernRegistry.AlgoliaSearchIndex, docsConfigInstanceId?: DocsV1Write.DocsConfigId, ): Promise { @@ -393,6 +425,7 @@ async function uploadToAlgolia( // we don't need to use this for generating algolia records filesV2: {}, apis: mapValues(apiDefinitionsById, (def) => convertDbAPIDefinitionToRead(def)), + apisV2: mapValues(apiDefinitionsLatestById, (def) => def), id: docsConfigInstanceId ?? DocsV1Write.DocsConfigId(""), search: getSearchInfoFromDocs({ algoliaIndex, @@ -409,6 +442,12 @@ async function uploadToAlgolia( lightModeEnabled: dbDocsDefinition.config.colorsV3?.type !== "dark", orgId: OrgId("dummy"), }; + + // TODO: consolidate this + const apis = + Object.entries(loadDocsForUrlResponse.definition.apis).length > 0 + ? FernNavigation.utils.toApis(loadDocsForUrlResponse) + : loadDocsForUrlResponse.definition.apisV2; await Promise.all( configSegmentTuples.map(async ([_, indexSegment]) => { try { @@ -416,7 +455,7 @@ async function uploadToAlgolia( indexSegmentId: indexSegment.id, nodes: FernNavigation.utils.toRootNode(loadDocsForUrlResponse), pages: FernNavigation.utils.toPages(loadDocsForUrlResponse), - apis: FernNavigation.utils.toApis(loadDocsForUrlResponse), + apis, isFieldRecordsEnabled: true, }); searchRecords.push( diff --git a/servers/fdr/src/db/api/APIDefinitionDao.ts b/servers/fdr/src/db/api/APIDefinitionDao.ts index 4d33b4f91b..e3be4f8824 100644 --- a/servers/fdr/src/db/api/APIDefinitionDao.ts +++ b/servers/fdr/src/db/api/APIDefinitionDao.ts @@ -1,4 +1,4 @@ -import { APIV1Db } from "@fern-api/fdr-sdk"; +import { APIV1Db, FdrAPI } from "@fern-api/fdr-sdk"; import { PrismaClient } from "@prisma/client"; import { readBuffer } from "../../util"; @@ -7,6 +7,8 @@ export interface APIDefinitionDao { loadAPIDefinition(apiDefinitionId: string): Promise; + loadAPILatestDefinition(apiDefinitionId: string): Promise; + loadAPIDefinitions(apiDefinitionIds: string[]): Promise>; } @@ -40,6 +42,23 @@ export class APIDefinitionDaoImpl implements APIDefinitionDao { return readBuffer(apiDefinition.definition) as APIV1Db.DbApiDefinition; } + public async loadAPILatestDefinition( + apiDefinitionId: string, + ): Promise { + const apiDefinition = await this.prisma.apiDefinitionsLatest.findFirst({ + where: { + apiDefinitionId, + }, + select: { + definition: true, + }, + }); + if (apiDefinition == null) { + return undefined; + } + return readBuffer(apiDefinition.definition) as FdrAPI.api.latest.ApiDefinition; + } + public async loadAPIDefinitions(apiDefinitionIds: string[]): Promise> { const apiDefinitions = await this.prisma.apiDefinitionsV2.findMany({ where: { diff --git a/servers/fdr/src/healthchecks/checkRedis.ts b/servers/fdr/src/healthchecks/checkRedis.ts index 01fe4c739f..1b5a4db982 100644 --- a/servers/fdr/src/healthchecks/checkRedis.ts +++ b/servers/fdr/src/healthchecks/checkRedis.ts @@ -16,6 +16,7 @@ const HEALTHCHECK_DOCS_RESPONSE: CachedDocsResponse = { definition: { pages: {}, apis: {}, + apisV2: {}, config: { navigation: { items: [], diff --git a/servers/fdr/src/server.ts b/servers/fdr/src/server.ts index 73131884e8..055ee14318 100644 --- a/servers/fdr/src/server.ts +++ b/servers/fdr/src/server.ts @@ -7,6 +7,7 @@ import { Agent, setGlobalDispatcher } from "undici"; import { register } from "./api"; import { FdrApplication, getConfig } from "./app"; import { registerBackgroundTasks } from "./background"; +import { getApiLatestService } from "./controllers/api/getApiLatestService"; import { getReadApiService } from "./controllers/api/getApiReadService"; import { getRegisterApiService } from "./controllers/api/getRegisterApiService"; import { getApiDiffService } from "./controllers/diff/getApiDiffService"; @@ -114,6 +115,9 @@ async function startServer(): Promise { _root: getRegisterApiService(app), }, }, + latest: { + _root: getApiLatestService(app), + }, }, snippets: getSnippetsService(app), snippetsFactory: getSnippetsFactoryService(app), diff --git a/servers/fdr/src/services/db/DatabaseService.ts b/servers/fdr/src/services/db/DatabaseService.ts index 63987cdfd3..21b4a27f24 100644 --- a/servers/fdr/src/services/db/DatabaseService.ts +++ b/servers/fdr/src/services/db/DatabaseService.ts @@ -1,4 +1,4 @@ -import { APIV1Db } from "@fern-api/fdr-sdk"; +import { APIV1Db, FdrAPI } from "@fern-api/fdr-sdk"; import { PrismaClient } from "@prisma/client"; export interface DatabaseService { @@ -6,6 +6,8 @@ export interface DatabaseService { getApiDefinition(id: string): Promise; + getApiLatestDefinition(id: string): Promise; + markIndexForDeletion(indexId: string): Promise; } @@ -28,6 +30,22 @@ export class DatabaseServiceImpl implements DatabaseService { } } + public async getApiLatestDefinition(id: string) { + const record = await this.prisma.apiDefinitionsLatest.findFirst({ + where: { + apiDefinitionId: id, + }, + }); + if (!record) { + return null; + } + try { + return JSON.parse(record.definition.toString()) as FdrAPI.api.latest.ApiDefinition; + } 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..29c0dfdb91 100644 --- a/servers/fdr/src/services/docs-cache/DocsDefinitionCache.ts +++ b/servers/fdr/src/services/docs-cache/DocsDefinitionCache.ts @@ -1,5 +1,6 @@ import { DocsV1Db, DocsV1Read, DocsV2Read } from "@fern-api/fdr-sdk"; import { AuthType } from "@prisma/client"; +import { FernRegistry } from "../../api/generated"; import { DomainNotRegisteredError } from "../../api/generated/api/resources/docs/resources/v2/resources/read"; import { FdrApplication } from "../../app"; import { getDocsDefinition, getDocsForDomain } from "../../controllers/docs/v1/getDocsReadService"; @@ -9,7 +10,6 @@ import type { IndexSegment } from "../algolia"; import { Semaphore } from "../revalidator/Semaphore"; import LocalDocsDefinitionStore from "./LocalDocsDefinitionStore"; import RedisDocsDefinitionStore from "./RedisDocsDefinitionStore"; -import { FernRegistry } from "../../api/generated"; const DOCS_DOMAIN_REGX = /^([^.\s]+)/;